[Developing on Nervos CKB] Introduction to Nervos CKB scripting [5]: Debugging debug

Author: Xuejie Original Link: https://xuejie.space/2019_10_18_introduction_to_ckb_script_programming_debugging/

Introduction to Nervos CKB scripting [5]: Debugging debug

In fact, CKB scripting works at a much lower level than other smart contracts, so the debugging process for CKB is fairly mysterious.In this article, we will show you how to debug CKB scripts.You will find that debugging CKB scripts is not really different from your daily debuggers.

This article is based on ckb v0.23.0.Specifically, I use the following version of commit in each project:

  • ckb: 7e2ad2d9ed6718360587f3762163229eccd2cf10
  • ckb-sdk-ruby: 18a89d8c69e173ad59ce3e3b3bf79b5d11c5f8f8
  • ckb-duktape:347bf730c08eb0aab7e56e0357945a4d6cee109a
  • ckb-standalone-debugger: 2379e89ae285e4e639b961756c22d8e4fde4d6ab

Debugging C Programs with GDB

The first scheme for debugging CKB scripts is usually applied to programming languages such as C and Rust.Maybe you're used to writing C programs, and GDB is your partner.You want to know if GDB can be used to debug C programs. The answer is of course: Yes!You can certainly debug CKB scripts written in C through GDB!Let me demonstrate:

First, let's use the carrot example we used in the previous article:

#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;
}

Here I made two changes:

First, I updated the script to make it compatible with ckb v0.23.0.In this version, we can use ckb_load_cell_data to get cell data.

I also added a minor bug to this code so that we can debug the workflow later.If you're very familiar with C, you may have noticed that of course you don't have to worry if you didn't realize it. I'll explain that later.

As always, we use the official toolchain to compile it into RISC-V code:

$ 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

Note that when I compile the script, I add -g to generate debug information, which is useful in GDB.For actual scripts, you always want to refine them to save as much space as possible on the chain.

Now let's deploy the script to CKB.Keep the CKB node running and start the 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))

Now that there is a carrot script on the chain, we can create a transaction to test the carrot script:

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))"}

If you examine the transaction carefully, you will find that there is no data in the output cell that starts with carrot.However, the validation still failed after we ran it, which means there must be a bug in our script.Previously, there was nothing else you could do. You might need to go back and check the code in the hope that you could find the error.But now that's not necessary, you can skip the transaction here and enter it into a separate CKB debugger to start debugging it!

First, let's dump the transaction, together with the environment used, into a local file:

pry(main)> CKB::MockTransactionDumper.new(api, tx).write("carrot.json")

Here you also need to track the hash of carrot type scripts:

pry(main)> carrot_type_script.compute_hash
=> "0x039c2fba64f389575cdecff8173882b97be5f8d3bdb2bb0770d8a7e265b91933"

Note that you may get a different hash than I do here, depending on the environment you use.

Now, let's try 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

Note that you may need to adjust the hash of a carrot-type script or the path of carrot.json depending on your environment.Now let's try connecting the debugger through GDB in a different terminal:

$ 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)

Note that 192.168.1.230 here is the IP address of my workstation on the local network. You may need to adjust the address because your computer may have a different IP address.Now let's try some of the common GDB debugging processes:

(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;

Here we can see where the problem is: the first byte in the buffer has a value of 0, which is different from c, so our buffer and carrot are different.The condition if (cap) {does not jump to the next loop, but to the true case, returns -1, indicating that it matches carrot.The reason for this problem is that when two buffers are equal, memcmp returns 0, and when they are not equal, it returns a non-zero value.But instead of testing whether the return value of memcmp is zero, we use it directly in the if condition, so that C treats all non-zero values as true, and the -99 returned here is true.For beginners, this is a typical error that you will encounter in C. I hope you will not make this error again.

Now that we know the cause of the error, it's easy to fix the error in the carrot script.But as you can see, we try to get the running state of a bad transaction from CKB and debug it with GDB, an industry-common tool.And is it great that your existing workflows and tools on GDB can also be used here?

Development/Debugging Based on REPL

However, GDB is only part of modern software development.Dynamic languages dominate to a large extent, and many programmers use REPL-based development/debugging workflows.This is completely different from GDB in the compilation language, basically you need a running environment where you can enter any code you want to interact with and get different results.As we will show here, CKB also supports this type of development/debugging workflow.

Here, we will use ckb-duktape to show the JavaScript-based REPL.Note, however, that this is just a demo that demonstrates the workflow and nothing prevents you from porting your favorite dynamic languages (whether Ruby, Rython, Lisp, and so on) to CKB and starting REPL for that language.

First, let's try compiling 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

You need to generate the build/repl binary here.Similar to the carrot example, we first deploy the duktape REPL binary on the 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))

We also need to create a deal that contains duktape scripts. I'll use a very simple script here, and of course you can add more data so you can play on 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"

Then let's save it to a file and check the hash of the duktape type script:

pry(main)> CKB::MockTransactionDumper.new(api, tx).write("duktape.json")
=> 2765824
pry(main)> duktape_repl_type_script.compute_hash
=> "0xa8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837"

Unlike the above, instead of starting the GDB, we can start the program directly:

$ ./target/release/ckb-debugger -g type -h 0xa8b79392c857e29cb283e452f2cd48a8e06c51af64be175e0fe0e2902c482837 -t duktape.json
duk>

You can see a DUK > prompting you to enter the JS code!Similarly, if you encounter an error, check to see if you need to change the hash of the type script or use the correct duktape.json path.We see common JS code that works here:

duk> print(1 + 2)
3
= undefined
duk> function foo(a) { return a + 1; }
= undefined
duk> foo(123)
= 124

You can also use functions related to 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

Note that the script hash we get here is the hash of the type of script we are currently executing!This will prove that CKB system debugging works here, and we can try something more interesting:

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

This 00e40b5402000000 may seem a little mysterious at first, but note that RISC-V uses little endian (low byte order), so if we invert the byte sequence here, we'll get 00000002540be400, which happens to be 10000000000 in decimal.Remember, capacity in CKB is shannons, so 10000000000 is exactly 100 bytes, which is the number of coins we want to send when we generate the transaction above!Now you see how to play happily with CKB in the duktape environment.

conclusion

We have described two different processes for debugging in CKB, and you can use either (or both) at will.I can't wait to see you play on CKB!

Join Nervos Community

Nervos Community is committed to becoming the best Nervos community. We will continue to promote and popularize Nervos technology, tap into Nervos'intrinsic value, open up the infinite possibilities of Nervos, and provide a quality platform for everyone who wants to learn more about Nervos Network.

Keywords: Blockchain JSON git Ruby github

Added by Michiel on Mon, 09 Dec 2019 05:23:47 +0200