虚拟机异常处理
异常类型
执行合约时,会出现四种类型的异常:
assert类型异常(断言异常)require类型异常- Validation 类型异常
- VM 非法操作类型异常
assert 类型异常
assert 类型异常以下情况将触发 assert 类型的异常,抛出 invalid opcode 错误,并消耗为该交易分配的全部 Energy(包括截至触发异常时已消耗的 Energy 与尚未消耗的剩余部分,即 fee_limit):
- 如果你访问数组的索引太大或为负数(例如 x[i] 其中 i >= x.length 或 i < 0)
- 如果你访问固定长度 bytesN 的索引太大或为负数
- 如果你用零当除数做除法或模运算(例如 5 / 0 或 23 % 0 )
- 如果你移位负数位
- 如果你将一个太大或负数值转换为一个枚举类型
- 如果你调用未被初始化的内部函数类型变量
- 如果你调用
assert的参数(表达式)最终结果是false - 合约执行过程中_超时_
- 发生
JVMStackOverFlowException - 发生
OutOfMem异常,即内存超过了 3M - 合约运行过程中,发生了加法等溢出
require 类型异常
require 类型异常下列情况将会产生一个 require 类型异常,会抛出 revert 错误,仅仅消耗当前为止已经消耗的 Energy,不包括未消耗的 Energy (fee_limit). 典型错误信息 REVERT opcode executed.
- 调用
throw - 如果你调用
require的参数(表达式)最终结果为false - 如果你通过消息调用某个函数,但该函数没有正确结束(比如该函数耗尽了 Energy,或者本身抛出一个异常)。如果调用函数时没有指定 Energy,会把所有 Energy 都传进去,表面看来会消耗所有 Energy,只有设置了 Energy 值,才能看出差别。该函数不包括低级别的操作
call、send、delegatecall或者callcode。低级操作不会抛出异常,而通过返回false来指示失败。 - 如果你使用 new 关键字创建合约,但合约没有正确创建(因为创建合约时无法指定 Energy,会把所有 Energy 都传进去,表面看来会消耗所有 Energy)
- 如果你的合约通过一个没有 payable 修饰符的公有函数(包括构造函数、
fallback函数和一般的公有函数)接收 TRX transfer()失败- 调用
revert() - 到达最大函数栈深 64
注:assert 类型和 require 类型这两种情况下,都会导致 TVM 回退。回退的原因是不能继续安全地执行,因为没有实现预期的效果。 因为我们想保留交易的原子性,所以最安全的做法是回退所有更改。但是会进行 Energy 扣费。
Validation 类型异常
下列情况将会产生一个 validation 类型异常,由于这些异常是虚拟机执行前的检测,该类异常交易不会上链,也不会消耗任何 Energy 或 Bandwidth.
- 当前版本不支持虚拟机
- 创建合约的时候,合约名字超出 32 字节
- 创建合约的时候,消耗调用者资源的比例不在 [0, 100] 之间
- 创建合约的时候,新生成的合约地址发生了 hash 冲突,即合约地址已经生成过
callvalue不为0,如果发生余额不足fee_Limit不在合法范围之内- 向不支持
constant的节点发送了constant的请求 - trigger 的合约在数据库中不存在
VM 非法操作类型异常
下列情况会发生一个 VM 非法操作类型异常,该类异常交易不会上链,但是会在网络层惩罚发送该交易的节点,断开连接一段时间。
- 创建合约的时候,
OwnerAddress和OriginAddress不相等 - 广播了一个
constant请求
异常处理流程 (Exception Handling Process)
- 入口是
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 中。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 {
}
}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 {
}
}Updated 17 days ago