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.
Before proceeding, you should be familiar with:
- CKB basics and the transaction structures
- Rust
- The
makecommand
Hello World
This section introduces the simplest contract, "Hello World," covering (Code):
- 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 ckb-rust-script
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 ckb-rust-script directory (Example). The generated project includes:
- An empty Rust project
- A
testsproject: A basic testing framework - A
Makefile: to execute common tasks usingmake - A
scriptsdirectory: 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 ckb-rust-script
make generate CRATE=hello-world
CRATE=[Contract Name]is not provided, you will be prompted to enter the project name during the setup.- Rust contracts must enable no_std. See details.
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
.debugending: 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
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 ( Code ).
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-testtoolshares the same underlying code asCKB, closely resembling the actual Nervos Network
The advantage of ckb-testtool:
- Allows direct command-line execution
- Supports debugging via
--mode gdbHowever,ckb-debuggerrequires manually constructing a transaction.
Recommended workflow:
- Use
ckb-testtoolfor regular testing - If issues arise, use
ckb-debuggerto 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": []
}
}
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)
To improve the readability of this JSON, the following modifications have been made:
- The random value for
Argshas been changed to0x11223344556677889900aabbccddeeff. - The random
tx_hashhas been replaced with a more structured value, such as0x0000000000000000000000000000000000000000000000000000000000000002.
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.