翻译自: https://justjjy.com/Build-CKB-contract-with-Rust-part-1
2020-01-06修改
删除链接器脚本部分,因为我发现没有必要自定义链接器
重构main方法接口
在CKB上部署合约最流行的方式是用C代码。在创世块中有3个默认的合约 secp256k1 lock
、 secp256k1 multisig lock
、Deposited DAO
,基本上每个使用CKB的人都在使用这些合约。
作为一个Rust语言爱好者,我们都想在任何场景下使用Rust。有个好消息,CKB虚拟机支持 RISC-V 指令集。最近在Rust中也增加对RISC-V的支持,这意味着我们可以直接将代码编译成RISC-V。然而,坏消息是RISC-V目标还不支持std库,这意味着你不能像通常那样使用Rust。
本系列文章向你展示了如何在Rust中编写CKB合约并部署。我们会发现,no_std
Rust其实比我们当初的印象要好。
本文假设你熟悉Rust并对CKB有一定的基础知识。你应该了解CKB的交易结构,并理解 类型脚本 和 锁定脚本。在本文中,用于描述类型脚本和锁定脚本的词是合约。
设定Rust环境
创建项目
初始化项目模版。我们创建2个项目 ckb-rust-demo
和 contract
。
ckb-rust-demo
是测试代码, contract
是合约代码。
cargo new --lib ckb-rust-demo
cd ckb-rust-demo
cargo new contract
安装 riscv64imac-unknown-none-elf
我们选择nightly Rust,因为需要几个不稳定的功能,然后我们安装RISC-V 平台。
# 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
编译第一个合约
cd contract
cargo build --target riscv64imac-unknown-none-elf
因为 riscv64imac-unknown-none-elf不支持std,所以编译失败。
修改 src/main.rs
添加 no_std
标记。
#![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() {}
让我们在重新编译代码
为了避免每次使用 --target
,我们在contract/.cargo/config
配置以下内容:
[build]
target = "riscv64imac-unknown-none-elf"
编译结果
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
测试合约
这个合约唯一做的就是返回 0
。这是一个正常的锁定脚本(它不完美,不要在 主网上部署这个合约)。
编写测试代码的基本思路是用我们的合约作为cell的锁定脚本,
合约返回0
,以为这任何人都可以花费这个 cell。
首先,我们使用合约作为锁定脚本模拟一个cell。构造一个交易使用cell,如果交易验证成功,则意味着锁定脚本正在工作。
添加ckb-contract-tool
作为依赖:
[dependencies]
ckb-contract-tool = { git = "https://github.com/jjyr/ckb-contract-tool.git" }
ckb-contract-tool
包含几个辅助方法。
以下测试代码写入ckb-rust-demo/src/lib.rs
:
#[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");
}
加载合约代码
建立上下文环境。
TxBuilder
帮助我们将模拟的Cell 注入上下文,并将合约作为cell的锁定脚本,然后构造一个交易来使用cell。验证
让我们试一下
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.
不用慌张,这个错误告诉我们,程序访问内存越界。
riscv64imac-unknown-none-elf
在处理入口点上有一点不同,使用 riscv64-unknown-elf-objdump -D
进行反汇编,可以发现没有.text
部分,我们必须找到除使用#[start]
之外的其他方法,来指示入口点。
定义入口点和main
让我们删除整个#[start]
函数,而是定义一个名为_start
的函数作为入口点:
#[no_mangle]
pub fn _start() -> ! {
loop{}
}
_start
的返回值是!
,这意味着这个函数永远不会返回;如果试图从该函数返回,则会得到一个InvalidPermission
的错误,因为入口点没有地方可以返回。
编译它
cargo build
# -> rust-lld: error: undefined symbol: abort
我们定义一个abort
函数来传递编译。
#[no_mangle]
pub fn abort() -> ! {
panic!("abort!")
}
编译在重复运行测试:
cargo build
cd ..
cargo tests
# ->
---- tests::it_works stdout ----
thread 'tests::it_works' panicked at 'pass test: Error { kind: ExceededMaximumCyclesScript }', src/libcore/result.rs:1188:5
当脚本周期超过最大周期限制时,会发生ExceededMaximumCycles
错误。为了退出程序,我们需要调用退出系统调用。
CKB-VM syscall
CKB环境支持多个 syscalls。
我们需要调用exit
系统调用退出程序,并返回一个退出码:
#[no_mangle]
pub fn _start() -> ! {
exit(0)
}
在Rust中调用exit
,我们需要写一些“有趣”的代码:
#![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 {}
}
a0
寄存器包含我们的第一个参数 _code
, a7
寄存器表示syscall的号码,93正是 exit
的syscall 好号码 。
编译并重新运行测试,这最后的工作了!
现在,你可以尝试搜索我们使用的每个不稳定的feature
,并尝试找出它的含义。尝试修改退出代码和_start
函数,重新运行测试看看发生了什么。
总结
这个demo的展示了如何使用Rust从底层的角度编写CKB合约。Rust的真正力量是语言的抽象能力和ta的工具链,这在本文中我们没有涉及。
例如,对于cargo,我们可以将库抽象到crates
中;如果我们可以导入一个syscalls crate,而不是自己编写,我们就可以得到一个更好的开发体验。更多的人在CKB上使用Rust,我们就可以使用更多的crates。
使用Rust的另一个好处是,在CKB中合约只进行验证。除了链上合约外,我们还需要编写一个链外代码来生成交易数据。如果我们为合约和off-chain生成器使用不同的语言,那么我们可能需要编写重复的代码,但是使用Rust,我们可以使用相同的库来编写合约和生成器。
用Rust写一个CKB合同可能看起来有点复杂;你可能会想,如果选择C,事情会变得更简单,目前来说,你是对的!
在下一篇文章中,我将向展示如何使用ckb-contract-std
库重写合约;你会发现这将会非常简单!
我们还将在以后的文章中讨论更多关于合约的问题。
参考:
https://github.com/jjyr/ckb-rust-demo
https://github.com/jjyr/ckb-contract-std
https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0019-data-structures/0019-data-structures.md
https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0009-vm-syscalls/0009-vm-syscalls.md