Rust contract part 1 - Build CKB contract with Rust
Edited at 2020-03-27
- Update ckg-std link
- Remove the linker script section since I found its unnecessary to customize linker
- Refactor the main function interface
AFAIK, the most popular contracts that deployed on CKB is writing in C. There are 3 default contracts in the genesis block: secp256k1 lock
, secp256k1 multisig lock
and Deposited DAO
, basically everyone uses CKB are using these contracts.
As a rustacean, I understand that you want to write everything in Rust. The good news is it’s possible, since CKB-VM supports RISC-V ISA(instruction set architecture), and recently the RISC-V target is added to Rust, which means we can directly compile our code to RISC-V. However, the bad news is that the RISC-V target is not supporting the std library yet, which means you can’t use Rust as a usual way.
This series of articles show you how to write a CKB contract in Rust and deploy it. We’ll see that the no_std
Rust is better than our first impression.
This article assumes you are familiar with Rust and have some basic knowledge of CKB. You should know the CKB transaction structure and understand what a type
script is and what a lock
script is. The word contract
used to describe both type
script and lock
script in this article.
Setup Rust environment
create a project
Let’s initial a project template. First, we create two projects: ckb-rust-demo
and contract
. The ckb-rust-demo
used to put our tests code, and the contract
used to put the contract code.
cargo new --lib ckb-rust-demo
cd ckb-rust-demo
cargo new contract
install riscv64imac-unknown-none-elf
target
We choose nightly Rust since several unstable features are required, then we install the RISC-V target.
# use nightly version rust
echo "nightly" > rust-toolchain
cargo version # -> cargo 1.41.0-nightly (626f0f40e 2019-12-03)
rustup target add riscv64imac-unknown-none-elf
Compile our first contract
Let’s try to compile the contract and see what happened:
cd contract
cargo build --target riscv64imac-unknown-none-elf
The compiling fails because of no std
for target riscv64imac-unknown-none-elf
.
Edit the src/main.rs
to notate no_std
flag.
#![no_std]
#![no_main]
#![feature(start)]
#![feature(lang_items)]
#[no_mangle]
#[start]
pub fn start(_argc: isize, _argv: *const *const u8) -> isize {
0
}
#[panic_handler]
fn panic_handler(_: &core::panic::PanicInfo) -> ! {
loop {}
}
#[lang = "eh_personality"]
extern "C" fn eh_personality() {}
The above code is a basic no_std
main, try compile again:
To avoid typing the --target
every time, we can write it into a config file contract/.cargo/config
then update the content:
[build]
target = "riscv64imac-unknown-none-elf"
Then build:
cargo build
file target/riscv64imac-unknown-none-elf/debug/contract
# -> target/riscv64imac-unknown-none-elf/debug/contract: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, with debug_info, not stripped
Test our contract
The only thing that the contract does is to return exit-code 0
. It’s perfect for a lock
script (it’s not perfect, don’t do it on the mainnet!).
The basic idea to write test code is to use our contract as a cell’s lock script, our contract return 0
, which means anyone can spend the cell.
First, we mock a cell with our contract as the lock script, then construct a transaction to spend the cell, if the transaction verification succeeded that means our lock script is working.
Add ckb-tool
as dependent:
[dependencies]
ckb-tool = { git = "https://github.com/jjyr/ckb-tool.git" }
ckb-tool
contains helper methods from several crates.
The test code which put in ckb-rust-demo/src/lib.rs
as below:
#[test]
fn it_works() {
// load contract code
let mut code = Vec::new();
File::open("contract/target/riscv64imac-unknown-none-elf/debug/contract").unwrap().read_to_end(&mut code).expect("read code");
let code = Bytes::from(code);
// build contract context
let mut context = Context::default();
context.deploy_contract(code.clone());
let tx = TxBuilder::default().lock_bin(code).inject_and_build(&mut context).expect("build tx");
// do the verification
let max_cycles = 50_000u64;
let verify_result = context.verify_tx(&tx, max_cycles);
verify_result.expect("pass test");
}
- Load contract code.
- Build a context. The
TxBuilder
helps us inject a mocked cell into the context with our contract as the cell’s lock script, then construct a transaction to spend the cell. - Do verification.
Let’s try it:
cargo test
# ->
---- tests::it_works stdout ----
thread 'tests::it_works' panicked at 'pass test: Error { kind: InternalError { kind: Compat { error: ErrorMessage { msg: "OutOfBound" } }
VM }
Internal }', src/libcore/result.rs:1188:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
Don’t panic! The error tells us our program access some memory that out of bound.
The riscv64imac-unknown-none-elf
target is little different on handling the entry point, use riscv64-unknown-elf-objdump -D <binary>
to disassembly we can find out that there no .text
section, we must find the other way to indicates the entry point other than using #[start]
.
Define the entry point and main
Let’s remove the entire #[start]
function, insteadly define a function with name _start
as entry point:
#[no_mangle]
pub fn _start() -> ! {
loop{}
}
The return value of _start
is !
, which means this function never returns; if you try to return from this function, you get an InvalidPermission
error, since the entry point has no place to return.
Let’s compile it:
cargo build
# -> rust-lld: error: undefined symbol: abort
We define an abort
function to passing the compile.
#[no_mangle]
pub fn abort() -> ! {
panic!("abort!")
}
Compile and run test again:
cargo build
cd ..
cargo tests
# ->
---- tests::it_works stdout ----
thread 'tests::it_works' panicked at 'pass test: Error { kind: ExceededMaximumCycles
Script }', src/libcore/result.rs:1188:5
ExceededMaximumCycles
error occurs when the script cycles exceed the max cycle limitation.
To exit the program, we need to invoke the exit
syscall.
CKB-VM syscall
The CKB environment supports several syscalls.
We need call exit
syscall to exit program and return a exit code:
#[no_mangle]
pub fn _start() -> ! {
exit(0)
}
To invoke syscall exit
from Rust, we need to write some interesting code:
#![feature(asm)]
...
/// Exit syscall
/// https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0009-vm-syscalls/0009-vm-syscalls.md
pub fn exit(_code: i8) -> ! {
unsafe {
// a0 is _code
asm!("li a7, 93");
asm!("ecall");
}
loop {}
}
The a0
register contains our first arg _code
according to the function calling convention, the a7
register indicates the syscall number, 93
is the syscall number of exit. We mark the return value with !
since exit
should never return.
Compile and rerun the test.
It finally works!
Now you can try to search each unstable feature
we used and try to figure out what it means. Try to modify the exit code and the _start
function, rerun the test see what happened.
Conclusion
The intention of this demo is trying to show you how to use Rust
to write a CKB contract from a low-level sight. The real power of Rust is the abstract ability of the language and the Rust toolchain, which we do not touch in this article.
For example, with cargo
, we can abstract libraries into crates; we gain a better developing experiment if we can just import a syscalls crate instead write it ourselves. More people use Rust
on CKB, more crates we can use.
Another advantage to use Rust is that in CKB, the contract only does verification. Aside from on-chain contracts, we also need to write an off-chain program to generate transaction data. That means we may need to write duplicated code if we use different languages, but with Rust, we can use the same code across the contract and the generator.
Write a CKB contract in Rust may seem a little bit complex; you may wonder the thing could get much more straightforward if you choose C, and you are right, just for now!
In the next article, I’ll show you how to rewrite our contract with ckb-std
library; you’ll surprise how simple thing goes.
That’s it. We’ll also discuss more serious contracts in later articles.