Home指南API 参考手册
指南API 参考手册社区Discord博客FAQ漏洞赏金公告中心English(英文版)Log In
指南

虚拟机异常处理

异常类型

执行合约时,会出现四种类型的异常:

  1. assert 类型异常(断言异常)
  2. require 类型异常
  3. Validation 类型异常
  4. VM 非法操作类型异常

assert 类型异常

以下情况将触发 assert 类型的异常,抛出 invalid opcode 错误,并消耗为该交易分配的全部 Energy(包括截至触发异常时已消耗的 Energy 与尚未消耗的剩余部分,即 fee_limit):

  1. 如果你访问数组的索引太大或为负数(例如 x[i] 其中 i >= x.length 或 i < 0)
  2. 如果你访问固定长度 bytesN 的索引太大或为负数
  3. 如果你用零当除数做除法或模运算(例如 5 / 0 或 23 % 0 )
  4. 如果你移位负数位
  5. 如果你将一个太大或负数值转换为一个枚举类型
  6. 如果你调用未被初始化的内部函数类型变量
  7. 如果你调用 assert 的参数(表达式)最终结果是 false
  8. 合约执行过程中_超时_
  9. 发生 JVMStackOverFlowException
  10. 发生 OutOfMem 异常,即内存超过了 3M
  11. 合约运行过程中,发生了加法等溢出

require 类型异常

下列情况将会产生一个 require 类型异常,会抛出 revert 错误,仅仅消耗当前为止已经消耗的 Energy,不包括未消耗的 Energy (fee_limit). 典型错误信息 REVERT opcode executed.

  1. 调用 throw
  2. 如果你调用 require 的参数(表达式)最终结果为 false
  3. 如果你通过消息调用某个函数,但该函数没有正确结束(比如该函数耗尽了 Energy,或者本身抛出一个异常)。如果调用函数时没有指定 Energy,会把所有 Energy 都传进去,表面看来会消耗所有 Energy,只有设置了 Energy 值,才能看出差别。该函数不包括低级别的操作 callsenddelegatecall 或者 callcode。低级操作不会抛出异常,而通过返回 false 来指示失败。
  4. 如果你使用 new 关键字创建合约,但合约没有正确创建(因为创建合约时无法指定 Energy,会把所有 Energy 都传进去,表面看来会消耗所有 Energy)
  5. 如果你的合约通过一个没有 payable 修饰符的公有函数(包括构造函数、fallback 函数和一般的公有函数)接收 TRX
  6. transfer() 失败
  7. 调用 revert()
  8. 到达最大函数栈深 64

注:assert 类型和 require 类型这两种情况下,都会导致 TVM 回退。回退的原因是不能继续安全地执行,因为没有实现预期的效果。 因为我们想保留交易的原子性,所以最安全的做法是回退所有更改。但是会进行 Energy 扣费。

Validation 类型异常

下列情况将会产生一个 validation 类型异常,由于这些异常是虚拟机执行前的检测,该类异常交易不会上链,也不会消耗任何 Energy 或 Bandwidth.

  1. 当前版本不支持虚拟机
  2. 创建合约的时候,合约名字超出 32 字节
  3. 创建合约的时候,消耗调用者资源的比例不在 [0, 100] 之间
  4. 创建合约的时候,新生成的合约地址发生了 hash 冲突,即合约地址已经生成过
  5. callvalue 不为0,如果发生余额不足
  6. fee_Limit 不在合法范围之内
  7. 向不支持 constant 的节点发送了 constant 的请求
  8. trigger 的合约在数据库中不存在

VM 非法操作类型异常

下列情况会发生一个 VM 非法操作类型异常,该类异常交易不会上链,但是会在网络层惩罚发送该交易的节点,断开连接一段时间。

  1. 创建合约的时候,OwnerAddressOriginAddress 不相等
  2. 广播了一个 constant 请求

异常处理流程 (Exception Handling Process)

  1. 入口是 go() 函数。所有的异常都会在 go() 内部被捕获和处理,不会向外层抛出。
public void go() {

    try {
      vm.play(program);

      result = program.getResult();

      // 如果存在 Exception 或 Revert
      // 重要提示:
      // Exception 是在 program 的设置中抛出的。 
      // Revert 是由虚拟机编译器提前写入字节码中的,通过跳转(jump)触发。 
      if (result.getException() != null || result.isRevert()) {

        if (result.getException() != null) {
          // 如果是 Exception,将消耗所有的 Energy
          program.spendAllEnergy();
          // 设置 runtimeError 字段以指示错误内容
          runtimeError = result.getException().getMessage();
          // 抛出异常
          throw result.getException();
        } else {
          // 如果是 Revert 且没有 Exception,只需设置 runtimeError 字段来指示错误内容。 
          runtimeError = "REVERT opcode executed";
        }

        // 只要发生 Exception 或 Revert,就不会执行 commit。虚拟机执行过程中的所有状态变更都不会落盘(保存)。 

      } else {
        // 如果没有 Exception 和 Revert,则执行 commit,虚拟机执行期间的所有状态变更都会落盘。 
        deposit.commit();
      }
    }
    catch (JVMStackOverFlowException e) {
        // TVM 或 JVM 发生 JVMStackOverFlowException,标记异常。 
        // JVMStackOverFlowException 只会在 go() 中被捕获,这通常发生在通过 call 调用的合约中,或者被循环调用的 JVMStackOverFlowException。它不会在 play() 中被捕获,只能在这里被捕获。 
        result.setException(e);
        runtimeError = result.getException().getMessage();
    }
    // 捕获所有可能抛出的内容 (Throwable)
    catch (Throwable e) {
      // 如果异常未知,则进行标记。 
      if (Objects.isNull(result.getException())) {
        result.setException(new RuntimeException("Unknown Throwable"));
      }
      // 确保 runtimeError 字段有值
      if (StringUtils.isEmpty(runtimeError)) {
        runtimeError = result.getException().getMessage();
      }

    }
  }
  // 执行完 go() 函数后,将不再使用 result.getException(),而是将 runtimeError 填充到 transactionInfo 中。
  1. play() 函数是虚拟机实际执行的地方。有三个地方会调用 play()go()(如上所述)、callToAddress()(执行 CALL 指令时调用,即在合约内部调用合约时),以及 createContract()(执行 CREATE 指令时,即在合约内部创建合约时)。后两者不会进行异常捕获处理,从 play() 中抛出的异常会在这两处继续向外抛出。
// play() 会捕获所有的 RuntimeException,并抛出 JVMStackOverFlowException(包括 StackOverflowError)
  public void play(Program program) {
    try {
      // 虚拟机的逐指令(op)执行循环
      while (!program.isStopped()) {
        // step() 会先捕获 RuntimeException,扣除 Energy,然后再抛出异常。 
        // 此时,它会停止 step(),先捕获 RuntimeException,然后扣除 Energy。 
        this.step(program);
      }
    }
    catch (JVMStackOverFlowException e) {
      // 抛出 JVMStackOverFlowException 异常
      throw new JVMStackOverFlowException();
    } catch (RuntimeException e) {
      if (StringUtils.isEmpty(e.getMessage())) {
        // 将 step() 函数抛出的 RuntimeException 放入 program.result.exception 中,而不是向外抛出。 
        program.setRuntimeFailure(new RuntimeException("Unknown Exception"));
      } else {
        program.setRuntimeFailure(e);
      }
    } catch (StackOverflowError soe) {
      // 抛出 JVMStackOverFlowException 异常
      throw new JVMStackOverFlowException();
    } finally {
    }
  }
  1. step() 函数
// step() 会先捕获 RuntimeException,扣除 Energy,然后再向外抛出
// 注意,这里抛出的异常全是 RuntimeException
public void step(Program program) {
  try {
    // 如果 op 非法,则抛出 IllegalOperationException。实际上,Invalid 是提前写入字节码中的,assert 类型的操作会跳转(jump)到 invalid。 
    OpCode op = OpCode.code(program.getCurrentOp());
    if (op == null) {
      throw Program.Exception.invalidOpCode(program.getCurrentOp());
    }
    switch (op) {
      // 1. 计算该 op 所需的 Energy

      // 2. 如果扣除时余额不足,则抛出 OutOfEnergyException
      program.spendEnergy(energyCost, op.name());
 
      // 3. 检测 CPU 时间,若超时则抛出 OutOfResourceException
      program.checkCPUTimeLimit(op.name());

      // 4. OP 的实际执行
      // 这里的重点是 CREATE 指令和 CALL 指令。 
      // 内部的执行步骤都非常相似: 
      // 4.1 当调用深度(depth)达到上限时,会将 0 压入栈(push 0 to stack),然后返回
      // 4.2 如果携带了 value 但余额不足,则将 0 压入栈,然后返回
      // 4.3 如果携带了 value 且转账失败,则抛出 RuntimeException 类型的异常。 
      // 4.4 (需要执行时)执行虚拟机
      // 4.5 若虚拟机执行结果发生异常:不会向外抛出异常,只会扣除所有的 Energy,并将 0 压入栈。 
      // 4.6 若虚拟机执行结果发生 revert:会退还 Energy,并将 0 压入栈。 
      // 4.7 执行成功:会退还 Energy,并将 1 压入栈。

      // 注意:
      // 发生异常后,到底是执行 Revert 操作还是其他处理,是由虚拟机的字节码决定的。有些会触发 revert,有些会触发 invalid。 
      // callToPrecompile 失败时,会直接抛出 RuntimeException 类型的异常
   }
 } catch (RuntimeException e) {
   // step() 会先捕获 RuntimeException,扣除所有的 Energy,然后再抛出
   program.spendAllEnergy();
   // 停止循环
   program.stop();
    // 抛出异常
    throw e;
  } finally {
  }
}