Skip to main content

Europa v0.2 报告

Patract Hub's treasury report for Europa v0.2

Patract Hub (https://patract.io) develops local open source toolkits and one-stop cloud smart IDE, committed to provide free development toolkits and infrastructure services for the entire smart contract ecosystem. Six weeks ago, we applied a treasury proposal for Europa v0.2 , and now we have finished the development (https://github.com/patractlabs/europa) . This repost shows what Europa v0.2 completion.

Introduction#

Europa is kind of another implementation for Substrate client in our design. We know that the runtime of a blockchain is the business logic that defines its behavior, and the Substrate runtime need to run by an executor and environment. So that we design the executor and environment more like a "sandbox" to run a Substrate runtime.

In v0.2, the primary target is to modify FRAME Contracts pallet in runtime to provide more detailed debugging information, including contract execution process information (in FRAME Contracts layer) and WASM execution information (in WASMI execution layer).

Summary of Europa's v0.2 plan:#

  1. Modify at FRAME Contracts pallet level to provide more information.

How to verify v0.2: Github source#

  • Construct incorrect contracts and execute logs printing to determine whether it meets expectations
  • Display the call statistics of the define_env! interface during contract execution
  • Execute the log printing function, provide formatted printing examples of different data, and judge whether it meets expectations
  • Construct a contract that crashes under different conditions and record the log after execution. Then judge whether the backtrace information of the contract execution is completely printed, and check whether it matches the actual execution of the collapsed contract.

后文统一称FRAME Contracts palletpallet-contracts

Design#

在0.2中,对于合约模块的调试信息功能的提升分为三个部分的修改:

  • pallet-contracts层的修改:在pallet-contracts的执行合约的过程中添加trace记录在合约层中的信息,并且记录合约的调用层级。另一方面完善了调用wasm执行的错误信息。
  • wasmi层的修改:给予wasmi提供了记录wasm执行中的backtrace功能,并且给parity-wasmpwasm-utilscargo-contract提供了支持在合约的wasm处理中包含name section的功能。
  • 合约日志功能:使用ChainExtensions的功能实现了合约中打印log日志的库。

当前在pallet-contracts中,在wasmi中执行出现错误以及wasmi执行过程中调用pallet-contractshost_function出现错误的时候,对外都表现为ContractTrap。这个情况对于开发者很难定位错误出现的原因,仅从这个信息无法分辨出出现问题的地方是合约自身,ink!,还是pallet-contracts。因此这个版本的europa提供的丰富的信息,能让开发者直接定位到出现问题的原因上。

pallet-contracts#

europa认为在合约调试过程需要:

  1. 丰富错误信息:wasm记录整个执行过程中出现错误信息的情况,包含wasm执行器的错误以及host_function的错误。wasmi的backtrace信息会和这里的错误信息统一起来。
  2. 调试过程中的执行情况:pallet-contracts的主要修改信息,以“合约栈”为记录合约调用合约的过程,以及当前这一层合约在执行过程中任何能辅助调试的信息,例如调用host_function的情况,selector,调用合约的参数等等。

因此针对这样的需求,europa做了如下设计与修改:

丰富错误信息#

  1. wasm执行器层的错误:

    europa设计了自己的ep-sandbox替换了原本pallet-contracts使用的sp-sandbox,并修改ep_sandbox::Error

    use patract_wasmi::Error as WasmiError;pub enum Error {    Module(WasmiError),    OutOfBounds,    Execution,    WasmiExecution(WasmiError),}

    Module(WasmiError)携带wasm层的原始错误信息,并且在 frame/contracts/src/wasm/runtime.rs 中的 to_execution_result转换为 String抛出错误信息。

    europa自己的ep-sandbox只有std版本(因为europa已经移除了所有的WASM的部分,不需要ep-sandbox支持no-std),因此在将来,ep-sandbox可以替换为不同的wasm执行器,以支持不同wasm执行器的运行测试及替换为支持debug的wasm执行器等特性。

    当前ep-sandbox使用的是一个forked版本的wasmi作为执行器,因此其抛出的错误为WasmiError。对于wasmi的错误改动见下一个章节。

  2. host_function的错误

    host函数执行错误会引起Trap,并且会记录TrapReason。对数据结构不进行修改,只需要记录即可。

调试过程中的执行情况#

europa forked 版本的pallet-contracts设计了一个对象记录合约执行中任何可以帮助调试的信息:

/// Record the contract execution context.pub struct NestedRuntime {    /// Current depth    depth: usize,    /// The current contract execute result    ext_result: ExecResult,    /// The value in sandbox successful result    sandbox_result_ok: Option<ReturnValue>,    /// Who call the current contract    caller: AccountId32,    /// The account of the current contract    self_account: Option<AccountId32>,    /// The input selector    selector: Option<HexVec>,    /// The input arguments    args: Option<HexVec>,    /// The value in call or the endowment in instantiate    value: u128,    /// The gas limit when this contract is called    gas_limit: Gas,    /// The gas left when this contract return    gas_left: Gas,    /// The host function call stack    env_trace: EnvTraceList,    /// The error in wasm    wasm_error: Option<WasmErrorWrapper>,    /// The trap in host function execution    trap_reason: Option<TrapReason>,    /// Nested contract execution context    nest: Vec<NestedRuntime>,}

pallet-contracts的模型中,合约调用合约是以“合约栈”的模型调用,因此NestedRuntime会跟踪整个合约栈的执行过程,并用nest这个属性保存一个NestedRuntime列表表示当前合约中调用过的其他合约。

pallet-contracts执行一个合约的过程中,europa以旁路的形态将执行过程中的相关信息记录在NestedRuntime这个结构体中,并在合约调用结束后将NestedRuntime以一定格式化的形式打印到日志中(将后文案例的展示)。合约的开发人员可以通过分析NestedRuntime打印的信息,得到此次合约执行过程中的各种详细信息,可以用于多种情况:

  1. 辅助定位错误出现的具体位置,包括以下情况:
    1. pallet-contracts层,
    2. ink!
    3. 合约层中的具体位置
    4. 在合约调用合约的情况下定位合约出现在哪一级合约中
  2. 分析此时合约执行过程中信息:
    1. 分析gas执行的消耗情况
    2. 分析get_storageset_storage调用的情况,帮助重构合约代码以及分析rent的需求情况
    3. 根据selectorargsvalue分析定位第三方sdk组建交易参数是否合法。
    4. 根据nest信息并结合seal_call信息分析合约调合约的执行路径。
    5. 等等...

pallet-contracts模块中记录到NestEdRuntime中的过程是比较细粒度的,以define_env!中的seal_call为例:

pub struct SealCall {    callee: Option<HexVec>,    gas: u64,    value: Option<u128>,    input: Option<HexVec>,    output: Option<HexVec>,}

其中的属性基本为Option<>,比如当调用合约前时会设置inputSome,调用合约正常后有返回值,则会设置output,如调用合约出现错误后,则output会保持None。因此若出现了inputSomeoutputNone的情况,则说明在合约调用合约的过程中,被调用的合约出现了问题。

当前NestedRuntime的信息只在日志中打印,将来NestedRuntime将会做本地化存储并提供相应的rpc供外部访问获取。因此在将来第三方应用可以获取NestedRuntime做进一步的处理,例如在Redspot中可以设计一个插件根据NestedRuntime的信息生成一个合约调用合约的拓扑图,在web钱包界面可以生成可视化合约调用路径等等。

wasmi#

我们fork了wasmi,并将这个forked版本的wasmi集成到了ep-sandbox中。而forked pallet-contracts可以通过ep-sandbox获取到forked wasmi的错误信息,包含wasmi的backtrace信息。

如果需要让wasmi具备能够保留执行的backtrace的信息,需要具备以下几点:

  1. WASM源文件中需要有"name section"段 (name section 的规范见 “Name Section”
  2. pallet-contracts检查合约的过程中保留“name section”信息且在处理过程后仍和wasm源文件具备对应关系。
  3. wasmi的执行过程中需要把执行栈保留下来并附带函数的关键信息,同时"name section"需要被解析,且和wasmi执行栈保留的函数信息对应起来。

对于2的改动,涉及cargo-buildparity-wasm,而对于1,3的改动主要位于forked 的wasmi中,少部分涉及pwasm-utils

1. Submitted WASM files contains debug info 提交的WASM源文件中有“name section”提供debug info#

Frist of all, we have to submit wasm files which contain the debug info that the on-chain side can parse and get the panic errors.

Currently, parity/cargo-contract trims debug info while building contracts, we can get them back with the following steps.

1.1 Keep name section from striping#

The name section has been striped at cargo-contract/cmd/build.rs#L247.

1.2 Keep debug info from wasm-opt#

cargo-contract passes debug_info: false to wasm-opt, so the debug-info will be always optimized when we run wasm-opt, the code is here: cargo-contract/cmd/build.rs#L267.

1.3 Keep debug info from rustc#

cargo-contract executes realease build by default, here we can enable debug build or modify the opt-level flag of rustc to keep debug infos at cargo-contract/cmd/build.rs#L137.

2. Save debug info from the scraper of pallet-contracts#

What happends to the pallet-contracts while we calling a contract?

  • Get the WASM binary from storage
  • Inject gas meter to the contract
  • Inject stack height to the contract
  • Put the contract into sp-sandbox and execute it
  • Get the result from sp-sandbox and return the result to us
2.1 Store name section while building WASM module#

pallet-contracts builds WASM modules from storage and drops custom sections by default, here we should get them back.

2.2 Update function indices in name section while injecting gas meter#

pallet-contracts reorders the indcies of functions in our WASM contracts after injecting gas memter to the WASM contracts at paritytech/wasm-utils/gas/mod.rs#L467, this will mess up the function indecies in name section that we can not get the correct backtraces.

3. Impelment WASM backtrace to WASMI#

Remember the last two steps in chapter 2, the core part of enabling WASM backtrace to pallet-contract is making wasmi support backtrace.

The process of executing a function in the interpreter of wasmi is like:

  • Invoke the target function
  • call and call and call over again
    • Panic if the process breaks.
3.1 Add function info field to FuncRef#

FuncRef is the 'function' wasmi interpreter calling directly, so we need to embed the debug info into the FuncRef as the first time, source at wasmi/func.rs#L26.

//! patractlabs/wasmi/src/func.rs#L26/// ...pub struct FuncRef {    /// ...    /// Function name and index for debug    info: Option<(usize, String)>,}
3.2 Set function info using name section while parsing WASM modules#

We alread have the info field in FuncRef, now we need to fill this field using name setciton while parsing WASM modules, source at wasmi/module#L343.

//! wasmi/src/module.rs#L343// ...if has_name_section {     if let Some(name) = function_names.get((index + import_count) as u32) {         func_instance = func_instance.set_info(index, name.to_string());     } else {         func_instance = func_instance.set_info(index, "<unknown>".to_string());     }}// ...
3.3 Make the interpreter support trace#

However, we don't need to get infos of every functions but the panicked series in the stack of the interpreter, source at wasmi/runner.rs#L1491.

//! wasmi/src/runner.rs#L1491/// Get the functions of current the stackpub fn trace(&self) -> Vec<Option<&(usize, String)>> {    self.buf.iter().map(|f| f.info()).collect::<Vec<_>>()}
3.4 Gather debug infos when program breaks#

Just gather backtraces when we invoke function failed, source at wasmi/func.rs#L170.

//! wasmi/src/func.rs#L170// ...
let res = interpreter.start_execution(externals);if let Err(trap) = res {let mut stack = interpreter    .trace()    .iter()    .map(|n| {        if let Some(info) = n {            format!("{:#}[{}]", rustc_demangle::demangle(&info.1), info.0)        } else {            "<unknown>".to_string()        }    })    .collect::<Vec<_>>();
// Append the panicing trapstack.append(&mut trap.wasm_trace().clone());stack.reverse();
// Embed this info into the trapErr(trap.set_wasm_trace(stack))
// ...

合约日志功能#

在合约调试的过程中需要知道合约执行内部的执行情况以及中间数据,在当前缺乏debug 调试条件的情况下(例如使用gdb进行调试),通过日志打印是最方便的方式。正如在europa v0.2 proposal中提到的,当前pallet-contractsink!已经支持了format!+seal_println的方式格式化打印字符串,但是通过这种模式有2个缺陷:

  1. seal_println的所有打印在节点端为target: runtime且级别DEBUG的日志,但是但开发复杂合约的时候将会打印很多的日志,如果不能通过target以及日志级别来过滤时,那么开发的过程中将会充斥着无关信息的干扰。
  2. 合约开发者在开发的过程中在需要的时候编写了seal_println,但是在合约发布的时候需要把seal_println全部删除。虽然合约开发者可以自己封装一个条件编译的函数来控制,但是若有工具库已经提供了这样的功能则更加方便。

因此europa提供了一个模仿rust的log crete的日志库ink-log来解决以上问题,其使用方式与rust中的log完全一致,减少了开发者的学习成本。

ink-log总体上通过pallet-contractsChainExtension实现,约定的的function_id0xfeffff00,通过结构体LoggerExt在wasm的memory中传递消息。因此这个库分为以下两部分:

  1. ink_log: 在ink-log/contracts目录下,给合约的编写提供info!, debug!, warn!, error!, trace!这几个与rust 的log库一样的宏,且宏的调用方式也与log完全一致。这些宏是对ink端的seal_chain_extensions的包装实现,是给合约开发者使用的工具库。例如在合约的Cargo.toml引入了这个库后,可以使用如下方式打印日志:

    Cargo.toml中:

    [dependencies]ink_log = { version = "0.1", git = "https://github.com/patractlabs/ink-log", default-features = false, features = ["ink-log-chain-extensions"] }
    [features]default = ["std"]std = [    # ...    "ink_log/std"]

    在合约中则可以使用如下方式在节点中打印日志:

    ink_log::info!(target: "flipper-contract", "latest value is: {}", self.value);
  2. runtime_log:在ink-log/runtime目录下,这个库是根据从ChainExtensions传递过来的function_idLoggerExt结构体的内容,调用frame_support里的debug下的相应日志进行打印。是给链的开发者准备的ink_log的实现库。例如链的开发者可以在自己的ChainExtensions中使用:

    Cargo.toml中:

    [dependencies]runtime_log = { version = "0.1", git = "https://github.com/patractlabs/ink-log", default-features = false }
    [features]default = ["std"]std = [    # ...    "runtime_log/std"]

    ChainExtensions的实现体中:

    pub struct CustomExt;impl ChainExtension for CustomExt {    fn call<E: Ext>(func_id: u32, env: Environment<E, InitState>) -> Result<RetVal, DispatchError>    where        <E::T as SysConfig>::AccountId: UncheckedFrom<<E::T as SysConfig>::Hash> + AsRef<[u8]>,    {        match func_id {            ... => {/* other ChainExtension */ }            0xfeffff00 => {                // TODO add other libs                runtime_log::logger_ext!(func_id, env);                // or use                // LoggerExt::call::<E>(func_id, env)                Ok(RetVal::Converging(0))            }        }       }}

ink_logruntime_log相对应,因此若合约开发者需要使用ink_log的时候,需要注意调试合约对应的链需要实现了runtime_log

另一方面合约开发者引入ink_log后,需要注意features = ["ink-log-chain-extensions"]ink_log只有开启了这个feature才会调用seal_chain_extensions与链进行交互。若没有这个features则会使用noop跳过合约打印的过程。

因此合约开发者可以通过features控制合约在调试环境和生产环境中打印日志,在调试环境中编译的合约开启"ink-log-chain-extensions"feature,而在生产环境编译的合约去掉这个feature。

What Europa can do in this version#

Build#

对于合约的开发者而言,需要准备europacargo-contract工具。

europa#

The building process for this project is as same as Substrate.

When building finished, current executable file in target directory is named europa.

git clone --recurse-submodules https://github.com/patractlabs/europa.git

cargo-contract#

若希望在europa的运行中看到WASM执行的backtrace,必须使用我们提供的cargo-contract版本。因为当前paritytech 仓库下 cargo-contract编译合约时使用了最高级别的优化等级,且丢弃了WASM中的“name section”部分。上文提到若需要打印wasmi执行合约中的调用栈信息必须要求合约文件具备“name section”的部分,因此使用paritytech提供的cargo-contract不能满足要求。我们在自己的fork的仓库里完成的这个功能,而另一方面我们认为让WASM具备“name section”的功能或许不会只有europa需要,因此我们向源仓库提交了pr#131[Enable debug info with flag in command build] 提供了这个功能。

在这个pr未合并前,当前只能使用我们(Patract Labs)提供的cargo-contract版本:

cargo install --git https://github.com/patractlabs/cargo-contract --branch cmd/debug --force

如果不希望这个版本的cargo-contract覆盖paritytech发布的版本,那么建议本地编译,并直接使用编译产物cargo-contract

git clone https://github.com/patractlabs/cargo-contract --branch cmd/debugcd cargo-contractcargo build --release

注:执行cargo-contract build 命令需要rust编译链的default toolchainnightly,否则只能使用cargo +nightly contract build,但是使用cargo调用cargo-contract需要执行cargo install安装或者将编译产物覆盖到~/.cargo/bin目录下,不能和paritytech的cargo-contract共存

执行

cargo-contract build --help# orcargo +nightly contract build --help

若能看到

FLAGS:    -d, --debug                  Emits debug info into wasm file

则表示正在使用Patract Labs提供的cargo-contract,若希望使用europa的过程中能看到WASM合约执行崩溃的backtrace,需要编译合约的时候加上--debug命令。

使用--debug命令将会在原本编译合约的target/ink目录中,生成除了正常文件以外的另一个文件,以 *.src.wasm结尾。这个*.src.wasm文件就是含有"name section"部分的WASM合约文件。

若需要使用europa做测试,则部署到europa的合约需要使用这个*.src.wasm文件,而不是原本生成的*.wasm文件。

example#

我们可以使用一个案例和其他已经遇到过的案例来验证europa定位问题的可靠性。

在后文中启动europa的方式均使用

$ europa --tmp -lruntime=debug

这种方式每次启动的 europa 都全新的数据。若想保留europa执行的数据,请参考europa 的README或者europa v0.1的报告,可以获得更多的命令介绍。

案例1:简单案例#

例如我们修改ink!项目中的案例合约ink/example/erc20如下:

#[ink(message)]pub fn transfer(&mut self, to: AccountId, value: Balance) -> Result<()> {    let from = self.env().caller();    self.transfer_from_to(from, to, value)?;    panic!("123");    Ok(())}

由于合约编译成WASM后对应的是原文件将宏展开后的代码,因此若想要对比调用栈的错误,需要将原合约的宏展开:

cargo install expandcd ink/example/erc20cargo expand > tmp.rs

通过阅读tmp.rs文件后,我们可以知道WASM执行到transfer函数时需要经过:

fn call() -> u32 -> <Erc20 as ::ink_lang::DispatchUsingMode>::dispatch_using_mode(...)-> <<Erc20 as ::ink_lang::ConstructorDispatcher>::Type as ::ink_lang::Execute>::execute(..)  # compile selector at here-> ::ink_lang::execute_message_mut-> move |state: &mut Erc20| { ... } # a closure-> <__ink_Msg<[(); 2644567034usize]> as ::ink_lang::MessageMut>::CALLABLE-> transfer

因此若合约调用过程中遇到了transfer中的panic时,WASM的backtrace应该和这个相近。

首先我们启动europa:

$ europa --tmp -lruntime=debug

然后我们部署这个erc20并调用transfer执行。

我们可以使用Polkadot/Substrate Portal或者使用RedSpot来验证这个过程。假设我们使用RedSpot来执行一次这个错误的ERC20合约的transfer调用,请注意在编译合约的过程中一定需要加上--debug子命令:

$ npx redspot-new erc20$ cd erc20/contracts# add panic in `transfer` function as abrove$ vim lib.rs # notice must add --debug when compile contract# due current cargo-contract is not paritytech, we need to copy compile product into `artifacts` directory. RedSpot would support europa and PatractLabs's cargo-contract in next version.$ cargo +nightly contract build --debug $ mkdir ../artifacts# notice must cp erc20.src.wasm, not erc20.wasm$ cp ./target/ink/erc20.src.wasm ../artifacts  # notice must rename metadata.json to erc20.json$ cp ./target/ink/metadata.json ../artifacts/erc20.json $ cd ../# enter redspot console, use `--no-compile` to deny recompile contract$ npx redspot console --no-compile  # in redspot consoleWelcome to Node.js v12.16.1.Type ".help" for more information.> > var factory = await patract.getContractFactory('erc20')# do following command could deploy the erc20 contract> var contract = await factory.deployed('new', '1000000', {value: 20000000000, salt:1})# do a transfer call directly> await contract.transfer("5GcTYx4dPTQfi4udKPvE4VKmbooV7zY6hNYVF9JXHJL4knNF", 100)

然后在europa的日志中,可以看到:

1: NestedRuntime {    ext_result: [failed] ExecError { error: DispatchError::Module {index:5, error:17, message: Some("ContractTrapped"), orign: ErrorOrigin::Caller }}    caller: d43593c715fdd31c61141abd04a99fd6822...(5GrwvaEF...),    self_account: b6484f58b7b939e93fff7dc10a654af7e.... (5GBi41bY...),    selector: 0xfae3a09d,    args: 0x1cbd2d43530a44705ad088af313e18f80b5....,    value: 0,    gas_limit: 409568000000,    gas_left: 369902872067,    env_trace: [        seal_value_transferred(Some(0x00000000000000000000000000000000)),        seal_input(Some(0xfae3a09d1cbd.....)),        seal_get_storage((Some(0x0100000000000....), Some(0x010000000100000001000000))),        # ...        seal_caller(Some(0xd43593c715fdd31c61141abd...)),        seal_hash_blake256((Some(0x696e6b20686173....), Some(0x0873b31b7a3cf....))),        # ...          seal_deposit_event((Some([0x45726332303a....00000000000]), Some(0x000..))),    ],    trap_reason: TrapReason::SupervisorError(DispatchError::Module { index: 5, error: 17, message: Some("ContractTrapped") }),    wasm_error: Error::WasmiExecution(Trap(Trap { kind: Unreachable }))        wasm backtrace:         |  core::panicking::panic[28]        |  erc20::erc20::_::<impl erc20::erc20::Erc20>::transfer[1697]        |  <erc20::erc20::_::__ink_Msg<[(); 2644567034]> as ink_lang::traits::MessageMut>::CALLABLE::{{closure}}[611]        |  core::ops::function::FnOnce::call_once[610]        |  <erc20::erc20::_::_::__ink_MessageDispatchEnum as ink_lang::dispatcher::Execute>::execute::{{closure}}[1675]        |  ink_lang::dispatcher::execute_message_mut[1674]        |  <erc20::erc20::_::_::__ink_MessageDispatchEnum as ink_lang::dispatcher::Execute>::execute[1692]        |  erc20::erc20::_::<impl ink_lang::contract::DispatchUsingMode for erc20::erc20::Erc20>::dispatch_using_mode[1690]        |  call[1691]        ╰─><unknown>[2387]    ,    nest: [],}

我们解释一下以上打印的信息:

  1. ext_result:表示这一次合约调用对外显示为执行成功或是失败:

    1. [success]:表示这次合约执行成功(注意:合约执行成功不代表着合约自身的业务逻辑执行成功,可能在ink!中或者合约自身业务逻辑中存在错误的返回,如后文中的案例3。)而[success]后面跟随的ExecResultValue { flag:0, data: 0x...}表示这次合约执行后的返回值。
    2. [failed]:表示这次合约执行失败,[failed]后面跟随的ExecError { .. } 表示这次错误的原因。这个原因是链上记录到event里面的值,也就是pallet-contractsdecl_error!中定义的值。
  2. 1: NestedRuntime &nest:表示当前打印信息的这个合约信息位于合约调用栈的第一层,若当前合约调用了另一个合约,则会在nest字段的数组里出现2: NestedRuntime1: NestedRuntime 里的结构一致。其中2标示这个被调用的合约位于合约调用栈的第二层。在当前合约中跨合约调用了几个合约,那么在nest的数组中就会出现几个NestedRuntime。若在第二层合约中有其他的合约调用,则以此类推。

    例如如果有合约A,B,C,如果是如下情况:

    1. A调用了B后,返回到A继续执行,然后又调用了合约C

      |A|| |->|B|| |<-| |->|C|| |<-

      那么就会产生类似如下的日志打印:

      1: NestedRuntime { self_account: A, nest:[     2: NestedRuntime {         self_account: B,         nest:[],     },     2: NestedRuntime {         self_account: C,         nest:[],     } ]}
    2. A调用了B后,B又调用了合约C,最后回到A中

      |A|| |->|B|| |  | |->|C|| |  | |<-| |<-

      那么就会产生类似如下的日志打印:

      1: NestedRuntime { self_account: A, nest:[     2: NestedRuntime {         self_account: B,         nest:[            3: NestedRuntime {                self_account: C,                nest:[],            }         ],     }   ]}
  3. caller:代表当前合约的调用者是谁。如果是合约调用合约的情况下,被调用的合约的这个值是上一级合约的地址。

  4. self_account:代表当前合约自身的地址是多少。

  5. selector & args&value :代表调用当前合约的时候传入的selector和参数,这些信息可以快速定位调用合约方式是否正确

  6. gas_limit & gas_left:表示当前调用合约的时候传入的gas_limit执行完这一层后剩余的gas。这里注意gas_left指代的是执行完这一层合约时剩余的gas,因此在合约调用合约中,通过gas_left可以判断出每一层合约消耗的gas,而不是只能获取到整个合约执行执行后的消耗。

  7. env_trace:表示当前这一层合约执行过程中,合约WASM执行每次调用host_function,就会在这里的数组里面添加一条记录。所有的host_function与pallet-contracts模块中的define_env!中的定义相对应,因此跟踪env_trace可以跟踪当前WASM合约执行过程中与pallet-contracts交互的过程。例如如果在env_trace中出现了:

    1. seal_call:代表当前合约中出现了合约调用合约的情况。按照seal_call出现在env_trace的顺序,可以和nest对应起来,推算出合约调用合约的前后状态。

    2. seal_get_storage&seal_set_storage:代表合约中出现了数据读写的情况。通过这两个接口,可以截获并统计当前合约执行过程中读写数据,而通过seal_set_storage统计出来的数据大小,也可以用来推测rent所需要的存储大小。

    3. seal_deposit_event:代表合约中出现了打印event的情况。这里可以分别截获每次event的内容,而不是最后得到一个统一的event。并且后文会通过一个例子表面通过europa可以快速定位host_function中出现bug的情况。

    另一方面env_trace的统计是比较细粒度的,例如在host_function中会出现多个错误可能的情况,则出现错误时,会将错误之前的信息全部保留,因此可以定位到host_function执行中出现问题的地点。

    而且若host_function中出现错误导致合约结束执行时,env_trace记录到的是最后一个出错的host_function调用,因此可以直接定位是哪一个host_function导致的合约执行异常。

  8. trap_reason:根据pallet-contracts中对于TrapReason的定义,trap_reason可以分为2类:

    1. Return & Termination & Restoration:表示合约退出是pallet-contracts的设计,并非内部出现错误。这一类Trap表示合约正常执行,并非错误。
    2. SupervisorError:表示合约调用host_function的执行过程中出现了错误。

    因此当前europa的日志打印设计为只要出现trap_reason就会记录下来。而另一方面,在合约的执行过程中并非一定会出现trap_reason。结合pallet-contracts以及ink!的设计,存在合约执行成功或者在ink!层中执行失败均不产生trap_reason的情况。因此europa除了记录trap_reason,还记录了WASM执行器执行后返回的结果,用sandbox_result_ok记录。

  9. sandbox_result_oksandbox_result_ok的值表示合约在WASM执行器执行后的结果。这个值本可以记录为sandbox_result,包含正确和错误两种情况。但是由于rust的限制,且结合pallet-contracts的业务逻辑,这里只保留sandbox_resultOk的结果。对于日志的打印,europa设计为只有当trap_reason是第一种情况时,打印sandbox_result_ok,作为辅助判断合约执行的信息。

    sandbox_result_ok是WASM执行器调用了invoke后的结果,在经过to_execution_result的处理后,如果没有trap_reason的情况分支中,Ok(..)的结果被丢弃了。但实际上这里包含两种情况:

    1. ink!中出现了错误:根据ink!的实现,在调用合约#[ink(message)]#[ink(constructor)]包装的函数之前,首先需要对输入的参数进行解码及匹配selector的过程。若在这个过程中出现错误,合约会返回错误码DispatchError。但是对于WASM执行器而言是正常的执行了WASM代码,因此会返回结果,包含这个错误码。这个合约执行过程属于错误情况。
    2. #[ink(message)]的返回值定义为了():根据ink!的实现,若返回值的类型是()则不会调用seal_reason,因此不会含有trap_reason这个合约执行过程属于正确情况。

    由于ink!只是运行于pallet-contracts的一种合约实现,其他的实现可能有不同的规则,因此当前sandbox_result_ok只用于辅助判定ink!合约的执行情况,值为ReturnValue。其中若日志的ReturnValue::Value(<num>)<num>部分不为0时,表示ink!的执行过程可能存在错误,可以根据ink!对于DispatchError的错误码判定错误。

  10. wasm_error:表示的是WASM执行错误时的backtrace。这个部分只有当ext_resultfailed的时候才会打印。

    在上面的例子中,由于执行transfer会触发panic,因此可以看到这里导致错误的原因是WasmiExecution(Trap(Trap { kind: Unreachable })),表示这次的失败是由于执行合约的过程中出现的Unreacble的情况导致,而下方的backtrace信息也十分准确的描述了上文讨论过的合约宏展开后遇到错误时的函数执行调用栈。从backtrace中可以明显的发现如下的调用过程。

    call -> dispatch_using_mode -> ... -> transfer -> panic 

    这个过程与合约的原文信息相符。

案例2:定位重复的topic导致的ContractTrap#

在前一段时间,我们(Patract Labs)给ink!汇报过一个bug,见issue:"When set '0' value in contracts event, may cause Error::ContractTrapped and panic in contract #589"。在europa还未开发出相关功能前,定位这个错误十分困难,其中感谢@athei定位到了错误。这里我们重现这个错误,并使用europa的日志来快速分析定位到这个bug出现的地方:

  1. ink!切换(checkout)到提交8e8fe09565ca6d2fad7701d68ff13f12deda7eed

    cd inkgit checkout 8e8fe09565ca6d2fad7701d68ff13f12deda7eed -b tmp
  2. 进入ink/examples/erc20/lib.rs:L90中将Transfer中的value改为0_u128

    #[ink(constructor)]pub fn new(initial_supply: Balance) -> Self {    //...    Self::env().emit_event(Transfer {        from: None,        to: Some(caller),        // change this from `initial_supply` to `0_u128`        value: 0_u128.into() // initial_supply,    });    instance}
  3. 执行cargo +nightly contract build --debug 编译合约

  4. 使用RedSpot或者Polkadot/Substrate Portal将这个合约部署出去(注意一定要使用erc20.src.wasm文件)

在部署阶段应该会遇到DuplicateTopics(在这个错误修正之前,汇报的错误是ContractTrap),而在europa的日志中会显示:

1: NestedRuntime {    #...    env_trace: [        seal_input(Some(0xd183512b0)),        #...            seal_deposit_event((Some([0x45726332303a3a5472616e736....]), None)),    ],    trap_reason: TrapReason::SupervisorError(DispatchError::Module { index: 5, error: 23, message: Some("DuplicateTopics") }),    wasm_error: Error::WasmiExecution(Trap(Trap { kind: Host(DummyHostError) }))        wasm backtrace:         |  ink_env::engine::on_chain::ext::deposit_event[1623]        |  ink_env::engine::on_chain::impls::<impl ink_env::backend::TypedEnvBackend for ink_env::engine::on_chain::EnvInstance>::emit_event[1564]        |  ink_env::api::emit_event::{{closure}}[1563]        |  <ink_env::engine::on_chain::EnvInstance as ink_env::engine::OnInstance>::on_instance[1562]        |  ink_env::api::emit_event[1561]        |  erc20::erc20::_::<impl ink_lang::events::EmitEvent<erc20::erc20::Erc20> for ink_lang::env_access::EnvAccess<<erc20::erc20::Erc20 as ink_lang::env_access::ContractEnv>::Env>>::emit_event[1685]# ...# ...        |  deploy[1691]        ╰─><unknown>[2385]    ,    nest: [],}

由以上日志可以看到:

  1. env_trace的最后一条记录是seal_deposit_event而不是seal_return(合约正确执行时最后一条一定是seal_return
  2. seal_deposit_event的第二个参数是None,而不是一个存在的值,因此表明seal_deposit_event 这个host_function没有执行完成,而是在执行过程中就出现了错误(可以看europa依赖的forked版本的pallet-contracts源码看到相应的实现方式)。
  3. 结合wasm backtrace的错误栈我们可以直观的看到backtrace最上一层调用栈为deposit_event

因此结合以上信息,我们可以直接推测出是seal_deposit_event这个host_function在执行的过程中出现了异常。(在pallet-contracts的提交pull#7762合并前,我们记录了host_function内的错误信息,合并后我们利用了trap_reason统一的错误信息。)

案例3,当链使用了type Balance=u64而不是type Balance=u128造成的错误。#

如果链使用了Balance=u64的定义,而对于ink!而言并不知道链关于Balance的定义,默认定义的Balance是u128。因此当使用u128定义Balanceink!作为依赖编译出的合约,运行在Balance定义为u64的链上时,会造成pallet-contracts模块向合约传值的时候,合约内部将u64value当做u128解码的错误。

以erc20的example合约为例,展开合约的宏后,可以看到:

  • dispatch_using_mode阶段解码input出现错误时,合约以::ink_lang::DispatchError::CouldNotReadInput返回,但是pallet-contracts的模型设计认为WASM的合约执行没有异常。
  • 而在call的调用时,由于在调用dispatch_using_mode之前首先会检查deny_payment,而检查deny_payment时若返回Error则直接panic

因此对于这种情况下,会出现部署(Instantiate)ERC20的合约执行正常,而调用ERC20的任意方法例如transfer的时候出现ContractTrap

对于这两种情况我们分别来看:

  1. instantiate阶段:

    1: NestedRuntime {    ext_result: [success] ExecError { error: DispatchError::Module {index:5, error:17, message: Some("ContractTrapped"), orign: ErrorOrigin::Caller }}#...    env_trace: [        seal_input(Some(0xd183512b008cb6611e0100000000000000000000)),        seal_caller(Some(0xd43593c715fdd31c61141abd04a99fd682)),        //...        seal_set_storage((Some(0x030000000100000...), Some(0x000000000000000...))),    ],    sandbox_result_ok: RuntimeValue::Value(7),    nest: [],}

    以上日志可以看到

    1. env_trace的最后不以seal_return结尾,则表示合约实际上没有正常执行完成。因为从ink!的设计中可以看到,如果正常进入到#[ink(constructor)]或者进入到#[ink(message)]则表面一定执行到了::ink_lang::execute_message中(::ink_lang::execute_message会调用seal_return),而没有出现seal_return代表没有执行到execute的阶段。

    2. sandbox_result_ok表示执行的返回值为7,查询ink!对于DispatchError的实现可以看到这个错误码代表着CouldNotReadInput

      DispatchError::CouldNotReadInput => Self(0x07),
    3. 根据合约宏展开的代码可以看到在dispatch_using_mode函数中,调用execute之前会调用::ink_env::decode_input,而这个函数存在return Error的情况。因此可以合理猜测是在解析input的时候出现异常。在日志中记录了输入参数args:0x008cb6611e0100000000000000000000,观察这个参数可以发现其长度明显小于u128编码的情况。因此可以根据argsenv_trace推测出是解码input的时候发生了错误。

      // this part code is expanded by erc20 example.::ink_env::decode_input::<<Erc20 as ::ink_lang::ConstructorDispatcher>::Type>().map_err(|_| ::ink_lang::DispatchError::CouldNotReadInput)?

    此时合约实例化成功,但是执行实例化的构造函数存在。因此合约存在于链上但是没有正常执行#[ink(constructor)]的过程。

  2. call阶段 ,如调用transfer:

    对以上实例化成功的函数进行调用transfer,会出现ContractTrap,europa的日志显示如下:

    1: NestedRuntime {    ext_result: [failed] ExecError { error: DispatchError::Module {index:5, error:17, message: Some("ContractTrapped"), orign: ErrorOrigin::Caller }}# ...    env_trace: [        seal_value_transferred(Some(0x0000000000000000)),    ],    wasm_error: Error::WasmiExecution(Trap(Trap { kind: Unreachable }))        wasm backtrace:         |  core::panicking::panic_fmt.60[1743]        |  core::result::unwrap_failed[914]        |  core::result::Result<T,E>::expect[915]        |  ink_lang::dispatcher::deny_payment[1664]        |  call[1691]        ╰─><unknown>[2387]    ,    nest: [],}

    首先注意到env_trace最后一个记录依然不是seal_return,且wasm_error的错误原因是WasmiExecution::Unreachable。因此可以确定是合约执行过程中遇到了panic或者expect

    而从wasm backtrace 中则十分明显的看到了在执行过程为

   call -> deny_payment -> expect

而根据宏展开的代码(cd ink/examples/erc20; cargo expand > tmp.rs)我们可以看到:

#[no_mangle]fn call() -> u32 {    if true {     ::ink_lang::deny_payment::<<Erc20 as ::ink_lang::ContractEnv>::Env>()            .expect("caller transferred value even though all ink! message deny payments")    }    ::ink_lang::DispatchRetCode::from(        <Erc20 as ::ink_lang::DispatchUsingMode>::dispatch_using_mode(            ::ink_lang::DispatchMode::Call,        ),    )    .to_u32()}

因此可以判定合约执行transfer的过程中在deny_payment返回了错误,而这里对错误直接处理为expect导致了wasmi执行结果为Unreachable。跟踪deny_payment的代码可以发现是该函数返回了Error导致的expect

注,相关代码如下:

ink_langhttps://github.com/paritytech/ink/blob/master/crates/lang/src/dispatcher.rs#L140-L150

pub fn deny_payment<E>() -> Result<()>where    E: Environment,{    let transferred = ink_env::transferred_balance::<E>()        .expect("encountered error while querying transferred balance");    if transferred != <E as Environment>::Balance::from(0u32) {        return Err(DispatchError::PaidUnpayableMessage)    }    Ok(())}

ink中此处的off_chain部分和on_chain部分会出现差异off_chain会认为在ink_env::transferred_balance::<E>()阶段就返回错误,于是在执行transferred_balance中后会遇到expect导致panic,而on_chain部分由于是从wasm的memory中取值,则会正常获取到对应u128长度的字符并解码得到transferred,只是解码的结果不会符合预期,导致transferred!=0deny_payment返回了Error,而在合约的宏展开调用deny_payment的部分才触发expect

if true {    ::ink_lang::deny_payment::<<Erc20 as ::ink_lang::ContractEnv>::Env>()        .expect("caller transferred value even though all ink! message deny payments")}

因此对于wasm backtrace 来说,expect是出现在call中调用deny_payment的时候,而不是deny_payment中调用transferred_balance的时候。

这个例子侧面说明ink!当前对于off_chain以及on_chain的处理并不是完全对应的,可能在一些情况下会给合约的使用者造成难以排查的错误

What we have implemented for v0.2#

Modify at FRAME Contracts pallet level to provide more information.

测试用例见: