Skip to main content

Quick Start

This chapter provides a step-by-step guide on developing contracts with Rust on CKB with several examples. As a "Quick Start" guide, it will not diving into in-depth details. For a more comprehensive understanding, please refer to the subsequent chapters.

note

Before proceeding, you should be familiar with:

  • CKB basics and the transaction structures
  • Rust
  • The make command

Hello World

This section introduces the simplest contract, "Hello World," covering:

  • Creating a project and contract
  • Common project commands
  • Running the contract

Create & Build

To create a project, use ckb-script-templates :

cargo generate gh:cryptape/ckb-script-templates workspace --name rust-script-examples
note

If --name [Project Name] is not provided, you will be prompted to enter the project name during the setup.

Upon successful execution, a Rust project will be created in the rust-script-examples directory. The generated project includes:

  • An empty Rust project
  • A tests project: A basic testing framework
  • A Makefile: to execute common tasks using make
  • A scripts directory: for storing utility Scripts

Create a Contract

The initialized project does not include a contract by default, so we need to generate one manually using make generate:

cd rust-script-examples
make generate CRATE=hello-world
note

CRATE=[Contract Name] is not provided, you will be prompted to enter the project name during the setup.

This command creates a hello-world subproject inside the contracts directory and automatically adds a test_hello_world function in tests/src/tests.rs.

The contract's entry function, program_entry (similar to main in Rust), is located in contracts/hello-world/src/main.rs. By default, the contract depends on ckb-std, which provides instruction support.

Insert the following code into program_entry:

ckb_std::debug!("Hello World!");

(Similar to println in the Rust Standard Library).

After implementing the code, compile all contracts using:

make build

The compiled contracts will be stored in the build/release directory, including:

  • A CKB contract file named after the contract
  • Files with .debug ending: They are contracts with debug information, which are larger and not suitable for testing or deployment.

Run & Test

Contracts can typically be executed in three ways:

  • Deploying on-chain and executing via an SDK (See here for reference)
  • Using ckb-debugger
  • Simulating an on-chain environment with ckb-testtool

This section covers the latter two methods: ckb-debugger and ckb-testtool.

Running with ckb-debugger:

ckb-debugger --bin build/release/hello-world

Output:

Script log: Hello World!
Run result: 0
All cycles: 7366(7.2K)

Running Tests

Run the test_hello_world test (Rust unit test):

cargo test -- tests::test_hello_world --nocapture

or use make:

make test CARGO_ARGS="-- tests::test_hello_world --nocapture"

Output:

---- tests::test_hello_world stdout ----
[contract debug] Hello World!
consume cycles: 7366
note

Formal contracts will get transaction information or on-chain data. For proper testing, it's best to script a complete transaction within tests. This will be covered in later chapters.

Simple Script (Print args data)

Real-world contracts often need to access transaction information or on-chain data. To build on the previous example, this section covers.

  • ckb-testtool: Adding logic code to existing tests.
  • ckb-debugger: Using --tx-file (or -f) to provide transaction data.

The advantage of ckb-testtool:

  • Allows modifying transaction details flexibly, making testing more convenient.
  • ckb-testtool shares the same underlying code as CKB, closely resembling the actual Nervos Network

The advantage of ckb-testtool:

  • Allows direct command-line execution
  • Supports debugging via --mode gdb However, ckb-debugger requires manually constructing a transaction.

Recommended workflow:

  • Use ckb-testtool for regular testing
  • If issues arise, use ckb-debugger to analyze transactions

Generate the Contract

Generate the contract using:

make generate CRATE=simple-print-args

Modify main.rs:

pub fn program_entry() -> i8 {
let script = ckb_std::high_level::load_script();
match script {
Ok(script) => {
let args = script.args().raw_data().to_vec();
ckb_std::debug!("Args Len: {}", args.len());
ckb_std::debug!("Args Data: {:02x?}", args);
0
}
Err(err) => {
ckb_std::debug!("load script failed: {:?}", err);
-1
}
}
}

This code retrieves and prints the contract args via ckb_std::high_level::load_script

Run With ckb-testtool

Executing test_simple_print_args produces:

[contract debug] Args Len: 1
[contract debug] Args Data: [2a]

In the automatically generated code, args is set to [42].

If we modify this section:

// prepare scripts
let lock_script = context
.build_script(&out_point, Bytes::from(vec![42]))
.expect("script");

to:

// prepare scripts
let args_data = ckb_testtool::context::random_hash().as_bytes();
let lock_script = context.build_script(&out_point, args_data).unwrap();

Output:

[contract debug] Args Len: 32
[contract debug] Args Data: [3a, b5, fb, 71, 5f, 11, 7a, 54, cf, 90, 7f, cf, 5d, d8, 5c, 05, 5a, 31, 8d, b5, b2, 7e, 2e, 41, 90, 57, 96, cd, 0b, de, b2, 60]

Here, we use ckb_testtool::context::random_hash() to generate a 32-byte random value, which is passed as an argument to the contract.

Run with ckb-debugger

Since this contract requires a transaction, executing it as before with:

ckb-debugger --bin build/release/simple-print-args

Output

Script log: Args Len: 0
Script log: Args Data: []
Run result: 0
All cycles: 18769(18.3K)

To resolve this, use --tx-file (or -f) to provide a transaction file. Since manually creating a transaction is cumbersome, we use ckb-testtool's dump_tx feature to generate one:

Before executing test_simple_print_args :

// run
let cycles = context
.verify_tx(&tx, 10_000_000)
.expect("pass verification");
println!("consume cycles: {}", cycles);

add the code

println!(
"{}",
&serde_json::to_string(&context.dump_tx(&tx).expect("dump tx info"))
.expect("tx format json")
);

This outputs a JSON representation of the transaction:

{
"mock_info": {
"inputs": [
{
"input": {
"since": "0x0",
"previous_output": {
"tx_hash": "0x13f0197e4b72ad3c60e229dc0043661dbd7dc9e569a2a94672da6494e52241f7",
"index": "0x0"
}
},
"output": {
"capacity": "0x3e8",
"lock": {
"code_hash": "0x3bb06b94457b32e22b874951710c100258a480321e31c709cb0cb176edee4fcb",
"hash_type": "type",
"args": "0xefc0aa60c7af052a4aec89d0196905e77b10d07bbbeceef824f2d905ee5b71b4"
},
"type": null
},
"data": "0x",
"header": null
}
],
"cell_deps": [
{
"cell_dep": {
"out_point": {
"tx_hash": "0xbb0ff58103ed8a6dfd16480c0e736dbc469a5cf38d111778e29d91d0e01cd9da",
"index": "0x0"
},
"dep_type": "code"
},
"output": {
"capacity": "0x3cb0b21aa00",
"lock": {
"code_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"hash_type": "data",
"args": "0x"
},
"type": {
"code_hash": "0x00000000000000000000000000000000000000000000000000545950455f4944",
"hash_type": "type",
"args": "0xa252bd68ecb370ab0005e7ddb4696792cbc51bd1c10ec3cc9079d08cd224b645"
}
},
"data": "...",
"header": null
}
],
"header_deps": [],
"extensions": []
},
"tx": {
"version": "0x0",
"cell_deps": [
{
"out_point": {
"tx_hash": "0xbb0ff58103ed8a6dfd16480c0e736dbc469a5cf38d111778e29d91d0e01cd9da",
"index": "0x0"
},
"dep_type": "code"
}
],
"header_deps": [],
"inputs": [
{
"since": "0x0",
"previous_output": {
"tx_hash": "0x13f0197e4b72ad3c60e229dc0043661dbd7dc9e569a2a94672da6494e52241f7",
"index": "0x0"
}
}
],
"outputs": [
{
"capacity": "0x1f4",
"lock": {
"code_hash": "0x3bb06b94457b32e22b874951710c100258a480321e31c709cb0cb176edee4fcb",
"hash_type": "type",
"args": "0xefc0aa60c7af052a4aec89d0196905e77b10d07bbbeceef824f2d905ee5b71b4"
},
"type": null
},
{
"capacity": "0x1f4",
"lock": {
"code_hash": "0x3bb06b94457b32e22b874951710c100258a480321e31c709cb0cb176edee4fcb",
"hash_type": "type",
"args": "0xefc0aa60c7af052a4aec89d0196905e77b10d07bbbeceef824f2d905ee5b71b4"
},
"type": null
}
],
"outputs_data": ["0x", "0x"],
"witnesses": []
}
}
note

The data field contains the contract binary, which is too long to display in this documentation; therefore, it is replaced by ....

Save this JSON as a file and execute the contract with ckb-debugger:

ckb-debugger -f tests/test-vectors/test_simple_print_args.json

Output

The cell_index is not specified. Assume --cell-index = 0
Script log: Args Len: 32
Script log: Args Data: [ef, c0, aa, 60, c7, af, 05, 2a, 4a, ec, 89, d0, 19, 69, 05, e7, 7b, 10, d0, 7b, bb, ec, ee, f8, 24, f2, d9, 05, ee, 5b, 71, b4]
Run result: 0
All cycles: 49239(48.1K)

Automating Transaction Updates

To avoid manually updating the transaction file every time the contract is modified, use macros:

"data": "0x{{ data ../../build/release/simple-print-args }}",

This converts the compiled contract into data.

Similarly, replace the Type Script definition with:

"type": "{{ def_type simple-print-args }}"

and reference the code_hash using:

"output": {
"capacity": "0x3e8",
"lock": {
"code_hash": "0x{{ ref_type simple-print-args }}",
"hash_type": "type",
"args": "0x11223344556677889900aabbccddeeff"
},
"type": null
},

This approach eliminates the need to update transaction details manually after each modification.

Final Execution

With the updated transaction file below:

{
"mock_info": {
"inputs": [
{
"input": {
"since": "0x0",
"previous_output": {
"tx_hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
"index": "0x0"
}
},
"output": {
"capacity": "0x3e8",
"lock": {
"code_hash": "0x{{ ref_type simple-print-args }}",
"hash_type": "type",
"args": "0x11223344556677889900aabbccddeeff"
},
"type": null
},
"data": "0x",
"header": null
}
],
"cell_deps": [
{
"cell_dep": {
"out_point": {
"tx_hash": "0x0000000000000000000000000000000000000000000000000000000000000002",
"index": "0x0"
},
"dep_type": "code"
},
"output": {
"capacity": "0x3cb0b21aa00",
"lock": {
"code_hash": "0x0000000000000000000000000000000000000000000000000000000000000003",
"hash_type": "data",
"args": "0x"
},
"type": "{{ def_type simple-print-args }}"
},
"data": "0x{{ data ../../build/release/simple-print-args }}",
"header": null
}
],
"header_deps": [],
"extensions": []
},
"tx": {
"version": "0x0",
"cell_deps": [
{
"out_point": {
"tx_hash": "0x0000000000000000000000000000000000000000000000000000000000000002",
"index": "0x0"
},
"dep_type": "code"
}
],
"header_deps": [],
"inputs": [
{
"since": "0x0",
"previous_output": {
"tx_hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
"index": "0x0"
}
}
],
"outputs": [
{
"capacity": "0x1f4",
"lock": {
"code_hash": "0x{{ ref_type simple-print-args }}",
"hash_type": "data2",
"args": "0x"
},
"type": null
}
],
"outputs_data": ["0x"],
"witnesses": []
}
}

Execute with ckb-debugger:

ckb-debugger -f tests/test-vectors/test_simple_print_args.json

Output:

The cell_index is not specified. Assume --cell-index = 0
Script log: Args Len: 16
Script log: Args Data: [11, 22, 33, 44, 55, 66, 77, 88, 99, 00, aa, bb, cc, dd, ee, ff]
Run result: 0
All cycles: 38030(37.1K)
note

To improve the readability of this JSON, the following modifications have been made:

  • The random value for Args has been changed to 0x11223344556677889900aabbccddeeff.
  • The random tx_hash has been replaced with a more structured value, such as 0x0000000000000000000000000000000000000000000000000000000000000002.

These changes do not affect the actual execution results.

The End

These two examples provide a simplified introduction to developing CKB Scripts with Rust. More advanced methods, such as additional ckb-std APIs and using gdb for debugging with ckb-debugger, are not covered here but will be explored in future chapters.