前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >本体技术视点 | 一文读懂Substrate的合约机制(三)

本体技术视点 | 一文读懂Substrate的合约机制(三)

作者头像
本体Ontology
发布2020-12-18 17:51:34
7780
发布2020-12-18 17:51:34
举报
文章被收录于专栏:本体研究院

上一期我们围绕 Substrate 的合约存储的收租机制、Wasm 合约限制、合约对外部交易的接口等方面展开了分析,本期即 Substrate 的合约机制的完结篇,我们将针对Wasm 合约运行时接口及 Wasm 合约的执行 Sandbox 机制进行讲解。

图片来源于网络

04

Wasm 合约运行时接口

合约运行过程中肯定需要和链交互,比如获取当前执行的区块高度,某个账户下的余额信息,合约数据的读写等,这是通过合约运行时 api 完成的。Wasm 可以通过 import 进行导入,类似于程序的动态链接库导入。目前提供的 api 大致如下,定义在 substrate/frame/contracts/src/wasm/runtime.rs 中:

代码语言:javascript
复制
//这个函数用于gas的计费,是合约代码部署时的预处理时插入的。因此正常的合约不能导入。
fn gas(amount: u32);

// 在指定的key下存储value,value的长度不能超过预定的值,且不能为空,key是定长的32字节,因此参数不需要指定长度。
fn seal_set_storage(key_ptr:u32, value_ptr: u32, value_len: u32);

// 清除指定的key的存储。
seal_clear_storage(key_ptr: u32);

// 获取指定key下的value值,结果存在out_ptr指定的内存中,out_len_ptr指向一个u32整数,是输入和输出参数,用于指定out_ptr指向内存buffer的长度,同时用于返回实际value对应的长度,函数返回值用于指明key是否存在。当out_len太小时会导致trap(其他函数也一样,只要发生越界会直接trap)。
fn seal_get_storage(key_ptr: u32, out_ptr: u32, out_len_ptr: u32) -> ReturnCode;

给某一个账户进行转账,account必须要能够解码为T::AccountId, value必须要能解码为T::Balance,否则直接trap,转账失败返回错误有`BelowSubsistenceThreshold`和`TransferFailed`。
fn seal_transfer(account_ptr: u32, account_len: u32, value_ptr: u32, value_len: u32) -> ReturnCode;

// 跨合约调用,当output_ptr为u32::max_value()时,表示忽略返回值。output_len_ptr是输入输出参数。存在如下错误码:`CalleeReverted`, `CalleeTrapped` `BelowSubsistenceThreshold` `TransferFailed` `NotCallable`
seal_call(callee_ptr, callee_len, gas, value_ptr, value_len,
    input_data_ptr, input_data_len, output_ptr, output_len_ptr: u32) -> ReturnCode

// 合约实例化
seal_instantiate(code_hash_ptr, code_hash_len, gas: u64, value_ptr, value_len, input_data_ptr, input_data_len, address_ptr: u32,
    address_len_ptr, output_ptr, output_len_ptr, salt_ptr, salt_len: u32) -> ReturnCode

// 销毁当前的合约,并将余额转到指定的账户,这个函数不会返回。
seal_terminate(beneficiary_ptr: u32, beneficiary_len: u32) ;

// 读取合约的输入,从实现上看,好像输入只能读取一次。
seal_input(buf_ptr: u32, buf_len_ptr: u32);

// 终止合约的执行,并返回结果。flags用于指定返回状态,目前只用了最低的一个bit,表示是否revert。其他的保留位必须为0,否则会导致trap。
seal_return(flags: u32, data_ptr: u32, data_len: u32) ;

// 获取调用方地址
seal_caller(out_ptr: u32, out_len_ptr: u32) 

// 获取合约自己的地址
seal_address(out_ptr: u32, out_len_ptr: u32) 

// 计算gas对应需要消耗的balance,注意不推荐使用小的gas参数,因为单位gas对应的balance可能小于1.
seal_weight_to_fee(gas: u64, out_ptr: u32, out_len_ptr: u32) 

//查询剩余的gas,为u32整数。
seal_gas_left(out_ptr: u32, out_len_ptr: u32) 

// 返回当前合约的余额
seal_balance(out_ptr: u32, out_len_ptr: u32) 

// 返回当前调用转的balance值。
seal_value_transferred(out_ptr: u32, out_len_ptr: u32)

// 根据当前的区块和subject获取一个随机值,结果是一个T::Hash
seal_random(subject_ptr: u32, subject_len: u32, out_ptr: u32, out_len_ptr: u32)

// 获取最后一个区块的时间戳
seal_now(out_ptr: u32, out_len_ptr: u32) 

// 获取账户不被删除所需要的最小balance
seal_minimum_balance(out_ptr: u32, out_len_ptr: u32) 

// 获取合约可以为tombsone状态存储的最小balance。注:为了使合约能够成为tombsone,其balance必须要大于tombsone deposit + existential deposit,代码中把两者之和称为subsistence threshold。
seal_tombstone_deposit(out_ptr: u32, out_len_ptr: u32);

// 恢复tombsone状态的合约
seal_restore_to(dest_ptr, dest_len, code_hash_ptr, code_hash_len, rent_allowance_ptr, 
    rent_allowance_len, delta_ptr, delta_count: u32) 

// 合约吐event,topics_ptr指向的是一个编码为Vec<T::Hash>的buffer,且内容不可重复。
seal_deposit_event(topics_ptr: u32, topics_len: u32, data_ptr: u32, data_len: u32) 

// 设置可以收取的最大租金额度
seal_set_rent_allowance(value_ptr: u32, value_len: u32) 

// 获取可收取的最大租金额度
seal_rent_allowance(out_ptr: u32, out_len_ptr: u32) 

// 打印log,只能用于调试链。
seal_println(str_ptr: u32, str_len: u32) 

// 获取当前区块号
fn seal_block_number(out_ptr: u32, out_len_ptr: u32) 

// 计算sha2-256/keccak-256/blake2-256/blake2-128的hash
fn seal_hash_sha2_256(input_ptr: u32, input_len: u32, output_ptr: u32)     
fn seal_hash_keccak_256(input_ptr: u32, input_len: u32, output_ptr: u32)
fn seal_hash_blake2_256(input_ptr: u32, input_len: u32, output_ptr: u32) 
fn seal_hash_blake2_128(input_ptr: u32, input_len: u32, output_ptr: u32)

05

Wasm 合约的执行 Sandbox 机制

上面所讲的 contract pallet 就是 Runtime 的一部分,因此如果 contract pallet 要执行 Wasm 合约,相当于是要在 Wasm 虚拟机里启动一个 Wasm 虚拟机跑 Wasm 合约的 code ,这中套娃操作显然是很低效的。为了解决这个问题, substrate 的 Host 开了一个 Sandbox 接口,实现了执行 Wasm 代码的功能,因此把执行 Wasm 合约和执行 Runtime 放置在了同一个层次,使执行的效率大大提高。

不过这也引入了新的困难:需要处理 host、runtime Wasm 合约三者之间的交互过程,其中比较复杂的就是合约 api 的调用机制。考虑合约调用交易的执行过程:

1. runtime 中的 contract pallet 根据交易指定的合约账户,加载合约代码;

2. runtime 根据 host 提供的 sandbox 接口将合约进行初始化,拿到 Wasm 模块实例化后的 instance handle;

3. runtime 根据构造调用参数,根据 host 提供的 sandbox 接口调用 Wasm 合约;

4. host 执行对应的 Wasm 合约;

5. Wasm 合约执行过程调用合约的存储接口 storage_put;

6. host 收到调用请求后,转发进入 runtime;

7. runtime 执行 storage_put,并返回结果给 host,host 进而返回给合约;

下面先看看 Sandbox 接口的定义,放在 substrate/primitives/wasm-interface/src/lib.rs 中,主要提供了 sandbox wasm 实例的创建和调用以及内存的读写功能:

代码语言:javascript
复制
pub trait Sandbox {
    /// 获取sandbox指定位置的内存
    fn memory_get(
        &mut self,
        memory_id: MemoryId,
        offset: WordSize,
        buf_ptr: Pointer<u8>,
        buf_len: WordSize,
    ) -> Result<u32>;
    /// 设置sandbox指定位置的内存
    fn memory_set(
        &mut self,
        memory_id: MemoryId,
        offset: WordSize,
        val_ptr: Pointer<u8>,
        val_len: WordSize,
    ) -> Result<u32>;
    /// 删除内存实例
    fn memory_teardown(&mut self, memory_id: MemoryId) -> Result<()>;
    /// 新建内存实例
    fn memory_new(&mut self, initial: u32, maximum: u32) -> Result<MemoryId>;
    /// 调用wasm中的某个导出函数
    fn invoke(
        &mut self,
        instance_id: u32,
        export_name: &str,
        args: &[u8],
        return_val: Pointer<u8>,
        return_val_len: WordSize,
        state: u32,
    ) -> Result<u32>;
    /// 删除wasm实例
    fn instance_teardown(&mut self, instance_id: u32) -> Result<()>;
    /// 创建sandbox wasm实例
    fn instance_new(
        &mut self,
        dispatch_thunk_id: u32,
        wasm: &[u8],
        raw_env_def: &[u8],
        state: u32,
    ) -> Result<u32>;

    /// 获取wasm实例的全局变量
    fn get_global_val(&self, instance_idx: u32, name: &str) -> Result<Option<Value>>;
}

Host 对 Sandbox 的实现在 substrate/client/executor/wasmtime 和substrate/client/executor/wasmi 中。(注:wasmtime 和 wasmi 都是 wasm 虚拟机的执行器,前者为 jit 执行,后者为解释执行,不过考虑到 jit 实现中 unsafe 代码过多,因此目前的 sandbox 实现都采用 wasmi)

其中 instance_new 方法的 dispatch_thunk_id,raw_env_def 和 state 是三者进行交互的关键,下面进行具体分析。

首先合约运行时 api 作为业务层,应该需要让 substrate 的开发者根据需要随时增减,因此肯定不能让 host 绑死,必然需要一定的动态机制。在 contract pallet 中通过 define_env 宏定义了 Wasm 合约可以导入调用的函数列表,即上面的 Wasm 合约运行时接口。根据这个列表可以构建出一个 Environment Definition Builder 结构,其实就是 Vec<(模块名,函数名,函数指针)>,这个列表会进行序列化,最终就是 instance_new 中的 raw_env_def 参数。host 收到这个参数后进行反序列化,得到所有运行时接口的列表,并根据这个构造 wasm 的 import resolver(注从代码上看这个阶段并没有拿到 runtime 函数的签名,所以 import resolver 没有检查签名而是直接放行了,因此后续执行阶段应该有签名检查的地方)。

因此合约运行时 api 已经有了,剩下的就是怎么调用的问题:host 和 runtime 约定让 runtime 提供一个函数,所有的合约 api 调用都通过这个函数中转,这个函数的签名大致如下:

代码语言:javascript
复制
fn dispatch_thunk(invoke_args_ptr, invoke_args_len: u32, state: u32, func_idx: u32) -> u64

invoke_args 是一系列 Wasm 基本类型参数序列化后的结果,state 是 runtime 调用 host 的 sandbox 接口时提供的,对应的是 substrate/frame/contracts/src/wasm/runtime.rs 中的 Runtime 结构,func_idx 就是要调用的合约运行时接口索引,返回值u64分解称高低两个 u32,分别表示执行后的 buffer 指针和长度。

dispatch_thunk 函数并没有在 runtime 中导出,而是放置在 runtime 的 table 中。通过在 instance_new 方法中 dispatch_thunk_id 参数传给 host。host 会根据这个 id 从 runtime 的 table 中进行查找,拿到后将合约调用的参数进行系列化,然后调用 runtime 的 dispatch_thunk 函数,完成动态派发过程。runtime 的 dispatch_thunk 函数定义在 substrate/primitives/sandbox/without_std.rs 中。runtime 的 dispatch_thunk 函数收到参数后进行反解,然后调用 func_idx 指定的定义在 define_env 中的函数。从 dispatch_thunk 的源码来看:

代码语言:javascript
复制
define_env!(
    fn seal_set_storage(ctx, key_ptr:u32, value_ptr: u32, value_len: u32);
)

生成的函数签名是:

代码语言:javascript
复制
fn seal_set_storage(ctx: &mut T, args:&[Value]);

因此解释了上面 host resolver 没有静态检查签名的原因,这个签名校验延迟到了函数执行反解参数的时候。

全篇完


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-12-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 本体研究院 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档