异常处理
执行合约时,会出现四种类型的异常:
- Assert-style异常
- Require-style异常
- Validation-style异常
- VMillegal-style异常
Assert-style异常
下列情况将会产生一个assert-style异常,会抛出invalid opcode错误,消耗所有的Energy(包括当前为止已经消耗的Energy和未消耗的Energy)
- 如果你访问数组的索引太大或为负数(例如 x[i] 其中 i >= x.length 或 i < 0)
- 如果你访问固定长度 bytesN 的索引太大或为负数
- 如果你用零当除数做除法或模运算(例如 5 / 0 或 23 % 0 )
- 如果你移位负数位
- 如果你将一个太大或负数值转换为一个枚举类型
- 如果你调用未被初始化的内部函数类型变量
- 如果你调用assert的参数(表达式)最终结果是false
- 合约执行过程中超时
- 发生JVMStackOverFlowException
- 发生OutOfMem异常,即内存超过了3M
- 合约运行过程中,发生了加法等溢出
Require-style异常
下列情况将会产生一个require-style异常,会抛出revert错误,仅仅消耗当前为止已经消耗的energy,不包括未消耗的Energy
- 调用throw
- 如果你调用require的参数(表达式)最终结果为false
- 如果你通过消息调用某个函数,但该函数没有正确结束(比如该函数耗尽了 Energy,或者本身抛出一个异常)。如果调用函数时没有指定Energy,会把所有Energy都传进去,表面看来会消耗所有Energy,只有设置了Energy值,才能看出差别。该函数不包括低级别的操作call、send、 delegatecall或者callcode 。低级操作不会抛出异常,而通过返回 false 来指示失败。
- 如果你使用new关键字创建合约,但合约没有正确创建(因为创建合约时无法指定Energy,会把所有Energy都传进去,表面看来会消耗所有Energy)
- 如果你的合约通过一个没有payable修饰符的公有函数(包括构造函数、fallback函数和一般的公有函数)接收TRX
- transfer() 失败
- 调用revert()
- 到达最大函数栈深64
注:assert-style和require-style这两种情况下,都会导致TVM 回退。回退的原因是不能继续安全地执行,因为没有实现预期的效果。 因为我们想保留交易的原子性,所以最安全的做法是回退所有更改。但是会进行扣费。
Validation-style异常
下列情况将会产生一个validation-style异常,该类异常下交易不上链,也不会消耗任何Energy
- 当前版本不支持虚拟机
- 创建合约的时候,合约名字超出32字节
- 创建合约的时候,消耗调用者资源的比例不在[0, 100]之间
- 创建合约的时候,新生成的合约地址发生了hash冲突,即合约地址已经生成过
- callvalue不为0,如果发生余额不足
- feeLimit不在合法范围之内
- 向不支持constant的节点发送了constant的请求
- trigger的合约在数据库中不存在
VMillegal-style异常
下列情况会发生一个VMillegal-style异常,该类异常交易不会上链,但是会在网络层惩罚发送该交易的节点,断开连接一段时间。
- 创建合约的时候,OwnerAddress和OriginAddress不相等
- 广播了一个constant请求
异常处理流程
- 入口是go()异常都会在go()里面接住并且处理,不会传到go()外面。
public void go() {
try {
vm.play(program);
result = program.getResult();
// 如果有Exception或者Revert
// 注意:
// Exception是程序中抛出设置的
// Revert是虚拟机编译器提前写进bytecode的,通过跳转到达
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异常,或者多次重复往下调用的JVMStackOverFlowException,在play()里面不会接住,只会在这里统一接住。
result.setException(e);
runtimeError = result.getException().getMessage();
}
// catch住所有能抛出的内容
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()函数是虚拟机真正执行的地方,有三个地方会调用到play(),go()(上面已经说明了)、callToAddress()(CALL指令会调用,也就是合约里面调用其他合约的时候)、createContract()(CREATE指令,也就是在合约里面创建合约的时候)。后两者不会做接住异常的处理,play里面的往外抛出的异常,在后两者里面会继续往外抛出。
// play里面会捕捉住所有的RuntimeException,会向外抛出JVMStackOverFlowException异常(包括StackOverflowError)
public void play(Program program) {
try {
// 一条op一条op的执行虚拟机
while (!program.isStopped()) {
// 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,其实是在bytecode中提前写好了Invalid,assert-style操作会跳转到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 深度到了,会push 0 到stack,return
// 4.2 有value的话,余额不足,则push 0 到stack,return
// 4.3 有value的话,transfer失败,抛出RuntimeException类型的异常
// 4.4 (需要执行的话)执行虚拟机
// 4.5 执行虚拟机的结果,有exception,不会抛出任何Exception,只会扣除所有Energy,并且push 0 到stack
// 4.6 执行虚拟机的结果,有revert,则会返还Energy,并且push 0 到stack
// 4.7 成功执行,会返还Energy,push 1 到stack
// 注意:
// revert操作,以及exception之后,怎么处理,是虚拟机bytecode决定的,有的会revert,有的会invalid
// callToPrecompile失败,会直接抛出RuntimeException类型的异常
}
} catch (RuntimeException e) {
// step会先接住RuntimeException,扣完Energy再抛出
program.spendAllEnergy();
// 停止循环
program.stop();
// 抛出异常
throw e;
} finally {
}
}Updated 9 months ago