使用Rust编译CKB合约 (一)

原文:https://www.jianshu.com/p/427741969246

2020-01-06修改

删除链接器脚本部分,因为我发现没有必要自定义链接器
重构main方法接口

在CKB上部署合约最流行的方式是用C代码。在创世块中有3个默认的合约 secp256k1 locksecp256k1 multisig lockDeposited 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-democontract
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");
}
  1. 加载合约代码

  2. 建立上下文环境。 TxBuilder 帮助我们将模拟的Cell 注入上下文,并将合约作为cell的锁定脚本,然后构造一个交易来使用cell。

  3. 验证

让我们试一下

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 <binary> 进行反汇编,可以发现没有.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/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

1 Like