使用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/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


Log in to reply