diff --git a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/signals/Exceptions.java b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/signals/Exceptions.java index 336318ec69..06ec00e0f8 100644 --- a/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/signals/Exceptions.java +++ b/arithmetization/src/main/java/net/consensys/linea/zktracer/module/hub/signals/Exceptions.java @@ -139,6 +139,15 @@ private static boolean isMemoryExpansionFault( private static boolean isOutOfGas(MessageFrame frame, OpCode opCode, GasProjector gp) { final long required = gp.of(frame, opCode).upfrontGasCost(); + System.out.println( + "opCode: " + + opCode.name() + + " ,required: " + + required + + " ,OOGX: " + + (required > frame.getRemainingGas()) + + " ,remainingGas: " + + frame.getRemainingGas()); return required > frame.getRemainingGas(); } diff --git a/arithmetization/src/test/java/net/consensys/linea/zktracer/exceptions/OutOfGasExceptionTest.java b/arithmetization/src/test/java/net/consensys/linea/zktracer/exceptions/OutOfGasExceptionTest.java index 25a045b26a..1520c313fc 100644 --- a/arithmetization/src/test/java/net/consensys/linea/zktracer/exceptions/OutOfGasExceptionTest.java +++ b/arithmetization/src/test/java/net/consensys/linea/zktracer/exceptions/OutOfGasExceptionTest.java @@ -26,28 +26,33 @@ import net.consensys.linea.testing.BytecodeCompiler; import net.consensys.linea.testing.BytecodeRunner; +import net.consensys.linea.testing.ToyAccount; +import net.consensys.linea.zktracer.module.constants.GlobalConstants; import net.consensys.linea.zktracer.opcode.OpCode; import net.consensys.linea.zktracer.opcode.OpCodeData; +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Wei; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; public class OutOfGasExceptionTest { - // TODO: add tests when address is warm. Use constants such as G_WARM_ACCESS etc + // TODO: add tests when address is warm for every opcode @ParameterizedTest @MethodSource("outOfGasExceptionSource") void outOfGasExceptionColdTest( - OpCode opCode, int staticCost, int nPushes, boolean triggersOutOfGasExceptions) { + OpCode opCode, int opCodeStaticCost, int nPushes, short corneCase) { BytecodeCompiler program = BytecodeCompiler.newProgram(); for (int i = 0; i < nPushes; i++) { program.push(0); } program.op(opCode); BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); - bytecodeRunner.run( - (long) 21000 + nPushes * 3L + staticCost - (triggersOutOfGasExceptions ? 1 : 0)); - if (triggersOutOfGasExceptions) { + // TODO: check if this approach is general enough, maybe use a similar approach to the test + // below + bytecodeRunner.run((long) 21000 + nPushes * 3L + opCodeStaticCost + corneCase); + if (corneCase == -1) { assertEquals( OUT_OF_GAS_EXCEPTION, bytecodeRunner.getHub().previousTraceSection().commonValues.tracedException()); @@ -62,18 +67,108 @@ static Stream outOfGasExceptionSource() { List arguments = new ArrayList<>(); for (OpCodeData opCodeData : opCodeToOpCodeDataMap.values()) { OpCode opCode = opCodeData.mnemonic(); - int staticCost = opCodeData.stackSettings().staticGas().cost(); - int delta = opCodeData.stackSettings().delta(); // number of items popped from the stack + int opCodeStaticCost = opCodeData.stackSettings().staticGas().cost(); + int nPushes = opCodeData.stackSettings().delta(); // number of items popped from the stack // TODO: some opCodes are excluded for now because they may need to be treated differently - if (staticCost > 0 + if (opCodeStaticCost > 0 && opCode != OpCode.MLOAD && opCode != OpCode.MSTORE8 && opCode != OpCode.SELFDESTRUCT && opCode != OpCode.MSTORE) { - arguments.add(Arguments.of(opCode, staticCost, delta, true)); - arguments.add(Arguments.of(opCode, staticCost, delta, false)); + arguments.add(Arguments.of(opCode, opCodeStaticCost, nPushes, -1)); + arguments.add(Arguments.of(opCode, opCodeStaticCost, nPushes, 0)); + arguments.add(Arguments.of(opCode, opCodeStaticCost, nPushes, 1)); } } return arguments.stream(); } + + @ParameterizedTest + @MethodSource("outOfGasExceptionCallSource") + void outOfGasExceptionCallTest( + int value, boolean targetAddressExists, boolean isWarm, short cornerCase) { + BytecodeCompiler program = BytecodeCompiler.newProgram(); + + if (targetAddressExists && isWarm) { + // Note: this is a possible way to warm the address + program.push("ca11ee").op(OpCode.BALANCE); + } + + program + .push(0) // return at capacity + .push(0) // return at offset + .push(0) // call data size + .push(0) // call data offset + .push(value) // value + .push("ca11ee") // address + .push(1000) // gas + .op(OpCode.CALL); + + BytecodeRunner bytecodeRunner = BytecodeRunner.of(program.compile()); + + long gasLimit = + 21000L + + // base gas cost + (isWarm ? 3L + 2600L : 0) // BALANCE + PUSH + + 7 * 3L // 7 PUSH + + callGasCost(value, targetAddressExists, isWarm); // CALL + + if (targetAddressExists) { + final ToyAccount calleeAccount = + ToyAccount.builder() + .balance(Wei.fromEth(1)) + .nonce(10) + .address(Address.fromHexString("ca11ee")) + .build(); + bytecodeRunner.run(gasLimit + cornerCase, List.of(calleeAccount)); + } else { + bytecodeRunner.run(gasLimit + cornerCase); + } + + if (cornerCase == -1) { + assertEquals( + OUT_OF_GAS_EXCEPTION, + bytecodeRunner.getHub().previousTraceSection().commonValues.tracedException()); + } else { + assertNotEquals( + OUT_OF_GAS_EXCEPTION, + bytecodeRunner.getHub().previousTraceSection().commonValues.tracedException()); + } + } + + static Stream outOfGasExceptionCallSource() { + List arguments = new ArrayList<>(); + for (int value : new int[] {0, 1}) { + for (short cornerCase : new short[] {-1, 0, 1}) { + arguments.add(Arguments.of(value, true, true, cornerCase)); + arguments.add(Arguments.of(value, true, false, cornerCase)); + arguments.add(Arguments.of(value, false, false, cornerCase)); + } + } + return arguments.stream(); + } + + private long callGasCost(int value, boolean targetAddressExists, boolean isWarm) { + // TODO: check if this method is correct, general enough and can be simplified + if (value == 0) { + if (isWarm) { + return GlobalConstants.GAS_CONST_G_WARM_ACCESS; + } else { + return GlobalConstants.GAS_CONST_G_COLD_ACCOUNT_ACCESS; + } + } else { + if (isWarm) { + return GlobalConstants.GAS_CONST_G_WARM_ACCESS + GlobalConstants.GAS_CONST_G_CALL_VALUE; + } else { + if (targetAddressExists) { + return GlobalConstants.GAS_CONST_G_COLD_ACCOUNT_ACCESS + + GlobalConstants.GAS_CONST_G_CALL_VALUE; + } else { + return GlobalConstants.GAS_CONST_G_NEW_ACCOUNT + + GlobalConstants.GAS_CONST_G_COLD_ACCOUNT_ACCESS + + GlobalConstants.GAS_CONST_G_CALL_VALUE; + } + } + } + } } diff --git a/testing/src/main/java/net/consensys/linea/testing/BytecodeRunner.java b/testing/src/main/java/net/consensys/linea/testing/BytecodeRunner.java index f38dbf78c6..d04c237337 100644 --- a/testing/src/main/java/net/consensys/linea/testing/BytecodeRunner.java +++ b/testing/src/main/java/net/consensys/linea/testing/BytecodeRunner.java @@ -87,6 +87,11 @@ public void run(List additionalAccounts) { this.run(Wei.fromEth(1), (long) GlobalConstants.LINEA_BLOCK_GAS_LIMIT, additionalAccounts); } + // Ad-hoc gasLimit and accounts + public void run(Long gasLimit, List additionalAccounts) { + this.run(Wei.fromEth(1), gasLimit, additionalAccounts); + } + // Ad-hoc senderBalance, gasLimit and accounts public void run(Wei senderBalance, Long gasLimit, List additionalAccounts) { checkArgument(byteCode != null, "byteCode cannot be empty");