diff --git a/actuator/src/main/java/org/tron/core/vm/program/Program.java b/actuator/src/main/java/org/tron/core/vm/program/Program.java index 869a9cc6e41..b00ae7532b7 100644 --- a/actuator/src/main/java/org/tron/core/vm/program/Program.java +++ b/actuator/src/main/java/org/tron/core/vm/program/Program.java @@ -992,8 +992,9 @@ this, new DataWord(newAddress), getContractAddress(), value, DataWord.ZERO(), byte[] code = createResult.getHReturn(); if (code.length != 0 && VMConfig.allowTvmLondon() && code[0] == (byte) 0xEF) { - createResult.setException(Program.Exception - .invalidCodeException()); + if (!createResult.isRevert() || !VMConfig.allowTvmResourceOptimization()) { + createResult.setException(Program.Exception.invalidCodeException()); + } } long saveCodeEnergy = (long) getLength(code) * EnergyCost.getCreateData(); diff --git a/actuator/src/test/java/org/tron/core/vm/program/ProgramCreateRevertTest.java b/actuator/src/test/java/org/tron/core/vm/program/ProgramCreateRevertTest.java new file mode 100644 index 00000000000..ba194efd289 --- /dev/null +++ b/actuator/src/test/java/org/tron/core/vm/program/ProgramCreateRevertTest.java @@ -0,0 +1,299 @@ +package org.tron.core.vm.program; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.function.Consumer; +import org.bouncycastle.util.encoders.Hex; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.MockedStatic; +import org.tron.common.runtime.InternalTransaction; +import org.tron.common.runtime.ProgramResult; +import org.tron.common.runtime.vm.DataWord; +import org.tron.common.runtime.vm.LogInfo; +import org.tron.core.vm.OperationRegistry; +import org.tron.core.vm.VM; +import org.tron.core.vm.config.VMConfig; +import org.tron.core.vm.program.invoke.ProgramInvoke; +import org.tron.core.vm.repository.Repository; + +@RunWith(Parameterized.class) +public class ProgramCreateRevertTest { + + private static final long ENERGY_LIMIT = 2_000_000L; + private static final long CHILD_ENERGY_LIMIT = 1_968_750L; + // Four PUSH1 instructions cost 12; legacy MSTORE8 charges only memory expansion, costing 3. + private static final long INIT_ENERGY = 15L; + private static final byte[] EMPTY = new byte[0]; + private static final byte[] EF = {(byte) 0xef}; + private static final byte[] EE = {(byte) 0xee}; + private final boolean create2; + private final boolean london; + private final boolean enabled; + private VMConfig.Snapshot snapshot; + private Repository repository; + private Repository childRepository; + private ProgramInvoke invoke; + private InternalTransaction root; + private Program parent; + + @Parameterized.Parameters(name = "create2={0}, london={1}, resourceOptimization={2}") + public static Collection parameters() { + Collection cases = new ArrayList<>(); + for (boolean create2 : new boolean[]{false, true}) { + for (boolean london : new boolean[]{false, true}) { + for (boolean enabled : new boolean[]{false, true}) { + cases.add(new Object[]{create2, london, enabled}); + } + } + } + return cases; + } + + public ProgramCreateRevertTest(boolean create2, boolean london, boolean enabled) { + this.create2 = create2; + this.london = london; + this.enabled = enabled; + } + + @Before + public void setUp() { + snapshot = new VMConfig.Snapshot(); + snapshot.allowTvmResourceOptimization = enabled; + snapshot.allowTvmLondon = london; + snapshot.allowTvmCompatibleEvm = true; + snapshot.allowTvmConstantinople = true; + snapshot.allowTvmIstanbul = true; + snapshot.allowTvmSolidity059 = true; + snapshot.allowTvmTransferTrc10 = true; + snapshot.allowMultiSign = true; + snapshot.allowEnergyAdjustment = true; + VMConfig.setLocalSnapshot(snapshot); + repository = mock(Repository.class); + childRepository = mock(Repository.class); + when(repository.newRepositoryChild()).thenReturn(childRepository); + invoke = mock(ProgramInvoke.class); + when(invoke.getDeposit()).thenReturn(repository); + when(invoke.getContractAddress()).thenReturn(new DataWord(1)); + when(invoke.getCallerAddress()).thenReturn(new DataWord(1)); + when(invoke.getOriginAddress()).thenReturn(new DataWord(1)); + when(invoke.getCallValue()).thenReturn(DataWord.ZERO()); + when(invoke.getPrevHash()).thenReturn(DataWord.ZERO()); + when(invoke.getCoinbase()).thenReturn(DataWord.ZERO()); + when(invoke.getTimestamp()).thenReturn(DataWord.ZERO()); + when(invoke.getNumber()).thenReturn(DataWord.ZERO()); + when(invoke.getDifficulty()).thenReturn(DataWord.ZERO()); + when(invoke.getEnergyLimit()).thenReturn(ENERGY_LIMIT); + when(invoke.getVmShouldEndInUs()).thenReturn(Long.MAX_VALUE); + root = mock(InternalTransaction.class); + when(root.getHash()).thenReturn(new byte[32]); + parent = newParent(false); + } + + @After + public void restoreConfig() { + VMConfig.clearLocalSnapshot(); + OperationRegistry.prepareAndGetTable(false); + } + + @Test + public void efRevertPreservesDataAndEnergyAfterActivation() { + execute(Hex.decode("60ef60005360016000fd")); + if (london && !enabled) { + assertOutcome(CHILD_ENERGY_LIMIT, EMPTY, false); + } else { + assertOutcome(INIT_ENERGY, EF, false); + } + } + + @Test + public void nonEfRevertPreservesDataAndEnergyInBothModes() { + execute(Hex.decode("60ee60005360016000fd")); + assertOutcome(INIT_ENERGY, EE, false); + } + + @Test + public void efReturnStillRejectsDeploymentWithLondon() { + execute(Hex.decode("60ef60005360016000f3")); + assertOutcome(london ? CHILD_ENERGY_LIMIT : INIT_ENERGY + 200L, EMPTY, !london); + } + + @Test + public void nonEfReturnStillDeploysCode() { + execute(Hex.decode("60ee60005360016000f3")); + assertOutcome(INIT_ENERGY + 200L, EMPTY, true); + } + + @Test + public void existingChildExceptionStillPreventsRefundAndCommit() { + RuntimeException exception = new Program.OutOfEnergyException("child energy exhausted"); + ProgramResult[] childResult = new ProgramResult[1]; + executeWithChild(child -> { + childResult[0] = revertedResult(child); + childResult[0].setException(exception); + }); + if (london && !enabled) { + assertTrue(childResult[0].getException() instanceof Program.InvalidCodeException); + } else { + assertSame(exception, childResult[0].getException()); + } + assertOutcome(CHILD_ENERGY_LIMIT, EMPTY, false); + } + + @Test + public void revertedChildEffectsAreRejectedAndNonceIsRetained() { + InternalTransaction nested = mock(InternalTransaction.class); + executeWithChild(child -> { + ProgramResult result = revertedResult(child); + result.addLogInfo(new LogInfo(new byte[21], Collections.emptyList(), new byte[]{1})); + result.addDeleteAccount(new DataWord(2)); + result.addInternalTransaction(nested); + child.setNonce(7); + }); + assertTrue(parent.getResult().getLogInfoList().isEmpty()); + assertTrue(parent.getResult().getDeleteAccounts().isEmpty()); + assertEquals(2, parent.getResult().getInternalTransactions().size()); + assertTrue(parent.getResult().getInternalTransactions().get(0).isRejected()); + assertSame(nested, parent.getResult().getInternalTransactions().get(1)); + verify(nested).reject(); + assertEquals(7, parent.getNonce()); + assertTrue(parent.getStack().pop().isZero()); + verify(childRepository, never()).commit(); + verify(childRepository, never()).saveCode(any(byte[].class), any(byte[].class)); + } + + @Test + public void expiredParentDeadlineTakesPriorityOverChildRevert() { + parent = newParent(true); + InternalTransaction nested = mock(InternalTransaction.class); + Runnable execute = () -> executeWithChild(child -> { + revertedResult(child).addInternalTransaction(nested); + child.setNonce(7); + }); + if (enabled) { + assertThrows(Program.OutOfTimeException.class, execute::run); + assertTrue(parent.getStack().isEmpty()); + assertEquals(1, parent.getNonce()); + assertEquals(1, parent.getResult().getInternalTransactions().size()); + assertEquals(CHILD_ENERGY_LIMIT, parent.getResult().getEnergyUsed()); + assertEquals(0, parent.getReturnDataBufferSize().intValue()); + verify(childRepository, never()).commit(); + } else { + execute.run(); + assertEquals(7, parent.getNonce()); + assertEquals(london ? CHILD_ENERGY_LIMIT : INIT_ENERGY, + parent.getResult().getEnergyUsed()); + assertTrue(parent.getStack().pop().isZero()); + } + } + + @Test + public void exceptionalReturnBufferRetainsExistingOsakaBoundary() throws Exception { + Field returnData = Program.class.getDeclaredField("returnDataBuffer"); + returnData.setAccessible(true); + for (boolean osaka : new boolean[]{false, true}) { + snapshot.allowTvmOsaka = osaka; + parent = newParent(false); + returnData.set(parent, new byte[]{(byte) 0xaa}); + executeWithChild(this::revertedResult); + byte[] expected = EF; + if (london && !enabled) { + // CREATE2 historically preserves old returndata before Osaka on exceptional failure. + expected = create2 && !osaka ? new byte[]{(byte) 0xaa} : EMPTY; + } + assertArrayEquals(expected, parent.getReturnDataBufferData(DataWord.ZERO(), + parent.getReturnDataBufferSize())); + verify(childRepository, never()).commit(); + } + } + + private Program newParent(boolean expired) { + // The continuation stores RETURNDATASIZE, making the child result observable in parent state. + Program program = new Program(Hex.decode("3d60005500"), new byte[21], invoke, root) { + @Override + public void checkCPUTimeLimit(String opName) { + if (expired) { + throw Program.Exception.notEnoughTime(opName); + } + super.checkCPUTimeLimit(opName); + } + }; + program.setRootTransactionId(new byte[32]); + // Contract version 1 keeps 1/64 of the Energy in the parent, even on child failure. + program.setContractVersion(1); + return program; + } + + private void execute(byte[] initCode) { + parent.memorySave(0, initCode); + OperationRegistry.prepareAndGetTable(false); + if (create2) { + parent.createContract2(DataWord.ZERO(), DataWord.ZERO(), new DataWord(initCode.length), + DataWord.ONE()); + } else { + parent.createContract(DataWord.ZERO(), DataWord.ZERO(), new DataWord(initCode.length)); + } + } + + private void executeWithChild(Consumer action) { + try (MockedStatic vm = mockStatic(VM.class)) { + vm.when(() -> VM.play(any(Program.class), any())).thenAnswer(invocation -> { + action.accept(invocation.getArgument(0)); + return null; + }); + execute(Hex.decode("60ef60005360016000fd")); + vm.verify(() -> VM.play(any(Program.class), any())); + } + } + + private ProgramResult revertedResult(Program child) { + ProgramResult result = child.getResult(); + result.setRevert(); + result.setHReturn(EF); + result.spendEnergy(INIT_ENERGY); + return result; + } + + private void assertOutcome(long energyUsed, byte[] returnData, boolean deployed) { + // These costs cover createContract*(), excluding the parent CREATE opcode's upfront cost. + assertEquals(energyUsed, parent.getResult().getEnergyUsed()); + assertArrayEquals(returnData, parent.getReturnDataBufferData(DataWord.ZERO(), + parent.getReturnDataBufferSize())); + assertEquals(!deployed, parent.getStack().pop().isZero()); + assertNull(parent.getResult().getException()); + assertFalse(parent.getResult().isRevert()); + assertEquals(1, parent.getNonce()); + assertEquals(1, parent.getResult().getInternalTransactions().size()); + assertEquals(!deployed, parent.getResult().getInternalTransactions().get(0).isRejected()); + if (deployed) { + verify(childRepository).commit(); + } else { + verify(childRepository, never()).commit(); + } + + VM.play(parent, OperationRegistry.prepareAndGetTable(false)); + assertNull(parent.getResult().getException()); + verify(repository).putStorageValue(any(byte[].class), eq(DataWord.ZERO()), + eq(new DataWord(returnData.length))); + } +} diff --git a/framework/src/main/java/org/tron/common/runtime/RuntimeImpl.java b/framework/src/main/java/org/tron/common/runtime/RuntimeImpl.java index 3dccfc5d146..c1febb6e637 100644 --- a/framework/src/main/java/org/tron/common/runtime/RuntimeImpl.java +++ b/framework/src/main/java/org/tron/common/runtime/RuntimeImpl.java @@ -5,6 +5,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.tron.core.ChainBaseManager; import org.tron.core.actuator.Actuator; import org.tron.core.actuator.Actuator2; import org.tron.core.actuator.ActuatorCreator; @@ -81,7 +82,9 @@ private void setResultCode(ProgramResult result) { result.setResultCode(contractResult.SUCCESS); return; } - if (result.isRevert()) { + + if (result.isRevert() && !ChainBaseManager.getInstance().getDynamicPropertiesStore() + .allowTvmResourceOptimization()) { result.setResultCode(contractResult.REVERT); return; } @@ -129,6 +132,10 @@ private void setResultCode(ProgramResult result) { result.setResultCode(contractResult.INVALID_CODE); return; } + if (result.isRevert()) { + result.setResultCode(contractResult.REVERT); + return; + } result.setResultCode(contractResult.UNKNOWN); } diff --git a/framework/src/test/java/org/tron/common/runtime/RuntimeImplMockTest.java b/framework/src/test/java/org/tron/common/runtime/RuntimeImplMockTest.java index e694f1c194f..03c4c9d7f3d 100644 --- a/framework/src/test/java/org/tron/common/runtime/RuntimeImplMockTest.java +++ b/framework/src/test/java/org/tron/common/runtime/RuntimeImplMockTest.java @@ -1,53 +1,85 @@ package org.tron.common.runtime; -import java.lang.reflect.Method; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; -import lombok.extern.slf4j.Slf4j; -import org.junit.After; +import java.lang.reflect.Method; import org.junit.Test; +import org.mockito.MockedStatic; +import org.tron.core.ChainBaseManager; +import org.tron.core.store.DynamicPropertiesStore; import org.tron.core.vm.program.Program; +import org.tron.protos.Protocol.Transaction.Result.contractResult; - - -@Slf4j public class RuntimeImplMockTest { - @After - public void clearMocks() { + @Test + public void nonRevertResultsDoNotReadChainProperties() throws Exception { + try (MockedStatic manager = mockStatic(ChainBaseManager.class)) { + assertResultCode(new ProgramResult(), contractResult.SUCCESS); + ProgramResult error = new ProgramResult(); + error.setRuntimeError("runtime error"); + assertResultCode(error, contractResult.UNKNOWN); + for (Object[] mapping : exceptionMappings()) { + ProgramResult result = new ProgramResult(); + result.setException((RuntimeException) mapping[0]); + assertResultCode(result, (contractResult) mapping[1]); + } + manager.verifyNoInteractions(); + } } @Test - public void testSetResultCode1() throws Exception { - RuntimeImpl runtime = new RuntimeImpl(); - ProgramResult programResult = new ProgramResult(); - Method privateMethod = RuntimeImpl.class.getDeclaredMethod( - "setResultCode", ProgramResult.class); - privateMethod.setAccessible(true); - - Program.BadJumpDestinationException badJumpDestinationException - = new Program.BadJumpDestinationException("Operation with pc isn't 'JUMPDEST': PC[%d];", 0); - programResult.setException(badJumpDestinationException); - privateMethod.invoke(runtime, programResult); - - Program.OutOfTimeException outOfTimeException - = new Program.OutOfTimeException("CPU timeout for 0x0a executing"); - programResult.setException(outOfTimeException); - privateMethod.invoke(runtime, programResult); - - Program.PrecompiledContractException precompiledContractException - = new Program.PrecompiledContractException("precompiled contract exception"); - programResult.setException(precompiledContractException); - privateMethod.invoke(runtime, programResult); - - Program.StackTooSmallException stackTooSmallException - = new Program.StackTooSmallException("Expected stack size %d but actual %d;", 100, 10); - programResult.setException(stackTooSmallException); - privateMethod.invoke(runtime, programResult); - - Program.JVMStackOverFlowException jvmStackOverFlowException - = new Program.JVMStackOverFlowException(); - programResult.setException(jvmStackOverFlowException); - privateMethod.invoke(runtime, programResult); + public void revertPriorityIsGatedByResourceOptimization() throws Exception { + DynamicPropertiesStore properties = mock(DynamicPropertiesStore.class); + ChainBaseManager manager = mock(ChainBaseManager.class); + when(manager.getDynamicPropertiesStore()).thenReturn(properties); + + try (MockedStatic managers = mockStatic(ChainBaseManager.class)) { + managers.when(ChainBaseManager::getInstance).thenReturn(manager); + for (boolean enabled : new boolean[]{false, true}) { + when(properties.allowTvmResourceOptimization()).thenReturn(enabled); + ProgramResult revert = new ProgramResult(); + revert.setRevert(); + assertResultCode(revert, contractResult.REVERT); + + for (Object[] mapping : exceptionMappings()) { + ProgramResult result = new ProgramResult(); + result.setRevert(); + result.setException((RuntimeException) mapping[0]); + contractResult exceptionCode = (contractResult) mapping[1]; + contractResult expected = enabled && exceptionCode != contractResult.UNKNOWN + ? exceptionCode : contractResult.REVERT; + assertResultCode(result, expected); + } + } + } } -} \ No newline at end of file + private Object[][] exceptionMappings() { + return new Object[][]{ + {new Program.IllegalOperationException("illegal"), contractResult.ILLEGAL_OPERATION}, + {new Program.OutOfEnergyException("energy"), contractResult.OUT_OF_ENERGY}, + {new Program.BadJumpDestinationException("jump"), contractResult.BAD_JUMP_DESTINATION}, + {new Program.OutOfTimeException("timeout"), contractResult.OUT_OF_TIME}, + {new Program.OutOfMemoryException("memory"), contractResult.OUT_OF_MEMORY}, + {new Program.PrecompiledContractException("precompile"), + contractResult.PRECOMPILED_CONTRACT}, + {new Program.StackTooSmallException("stack"), contractResult.STACK_TOO_SMALL}, + {mock(Program.StackTooLargeException.class), contractResult.STACK_TOO_LARGE}, + {new Program.JVMStackOverFlowException(), contractResult.JVM_STACK_OVER_FLOW}, + {new Program.TransferException("transfer"), contractResult.TRANSFER_FAILED}, + {new Program.InvalidCodeException("invalid"), contractResult.INVALID_CODE}, + {new RuntimeException("unknown"), contractResult.UNKNOWN} + }; + } + + private void assertResultCode(ProgramResult result, contractResult expected) throws Exception { + Method method = RuntimeImpl.class.getDeclaredMethod("setResultCode", ProgramResult.class); + method.setAccessible(true); + method.invoke(new RuntimeImpl(), result); + assertEquals(expected, result.getResultCode()); + } +} diff --git a/framework/src/test/java/org/tron/common/runtime/RuntimeImplRevertTimeoutTest.java b/framework/src/test/java/org/tron/common/runtime/RuntimeImplRevertTimeoutTest.java new file mode 100644 index 00000000000..31a3712d026 --- /dev/null +++ b/framework/src/test/java/org/tron/common/runtime/RuntimeImplRevertTimeoutTest.java @@ -0,0 +1,181 @@ +package org.tron.common.runtime; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collection; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.MockedStatic; +import org.springframework.test.util.ReflectionTestUtils; +import org.tron.common.VMConfigRule; +import org.tron.common.runtime.InternalTransaction.TrxType; +import org.tron.common.runtime.vm.DataWord; +import org.tron.core.ChainBaseManager; +import org.tron.core.actuator.VMActuator; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.db.TransactionContext; +import org.tron.core.db.TransactionTrace; +import org.tron.core.exception.ReceiptCheckErrException; +import org.tron.core.store.DynamicPropertiesStore; +import org.tron.core.store.StoreFactory; +import org.tron.core.vm.Op; +import org.tron.core.vm.config.VMConfig; +import org.tron.core.vm.program.Program; +import org.tron.core.vm.program.invoke.ProgramInvoke; +import org.tron.core.vm.repository.Repository; +import org.tron.protos.Protocol.Transaction; +import org.tron.protos.Protocol.Transaction.Contract.ContractType; +import org.tron.protos.Protocol.Transaction.Result.contractResult; + +@RunWith(Parameterized.class) +public class RuntimeImplRevertTimeoutTest { + + private static final long ENERGY_LIMIT = 100_000L; + private final boolean enabled; + private final ContractType contractType; + private StoreFactory stores; + private MockedStatic managers; + + @Rule + public final VMConfigRule vmConfigRule = new VMConfigRule(); + + @Parameterized.Parameters(name = "resourceOptimization={0}, contractType={1}") + public static Collection parameters() { + return Arrays.asList(new Object[][]{ + {false, ContractType.TriggerSmartContract}, + {true, ContractType.TriggerSmartContract}, + {false, ContractType.CreateSmartContract}, + {true, ContractType.CreateSmartContract} + }); + } + + public RuntimeImplRevertTimeoutTest(boolean enabled, ContractType contractType) { + this.enabled = enabled; + this.contractType = contractType; + } + + @Before + public void setUp() { + VMConfig.Snapshot snapshot = new VMConfig.Snapshot(); + snapshot.allowTvmResourceOptimization = enabled; + snapshot.allowEnergyAdjustment = true; + VMConfig.setLocalSnapshot(snapshot); + VMConfig.setVmTrace(false); + + DynamicPropertiesStore properties = mock(DynamicPropertiesStore.class); + when(properties.allowTvmResourceOptimization()).thenReturn(enabled); + when(properties.getLatestVersion()).thenReturn(1); + ChainBaseManager manager = mock(ChainBaseManager.class); + when(manager.getDynamicPropertiesStore()).thenReturn(properties); + stores = mock(StoreFactory.class); + when(stores.getChainBaseManager()).thenReturn(manager); + managers = mockStatic(ChainBaseManager.class); + managers.when(ChainBaseManager::getInstance).thenReturn(manager); + } + + @After + public void tearDown() { + if (managers != null) { + managers.close(); + } + } + + @Test + public void localPostRevertTimeoutRequestsRetryAfterActivation() throws Exception { + ProgramResult result = executeRevert(true); + assertTrue(result.isRevert()); + assertTrue(result.getException() instanceof Program.OutOfTimeException); + assertEquals(ENERGY_LIMIT, result.getEnergyUsed()); + assertEquals(enabled ? contractResult.OUT_OF_TIME : contractResult.REVERT, + result.getResultCode()); + + TransactionTrace trace = trace(result, contractResult.REVERT); + assertEquals(ENERGY_LIMIT, trace.getReceipt().getEnergyUsageTotal()); + assertEquals(enabled, trace.checkNeedRetry()); + if (enabled) { + assertThrows(ReceiptCheckErrException.class, trace::check); + } else { + trace.check(); + } + + TransactionTrace matching = trace(result, result.getResultCode()); + assertFalse(matching.checkNeedRetry()); + matching.check(); + } + + @Test + public void producerTimeoutCannotSilentlyMatchNormalRevertAfterActivation() throws Exception { + ProgramResult producer = executeRevert(true); + ProgramResult validator = executeRevert(false); + assertTrue(validator.isRevert()); + assertNull(validator.getException()); + assertEquals(contractResult.REVERT, validator.getResultCode()); + assertEquals(ENERGY_LIMIT, producer.getEnergyUsed()); + assertEquals(6L, validator.getEnergyUsed()); + + TransactionTrace trace = trace(validator, producer.getResultCode()); + assertFalse(trace.checkNeedRetry()); + if (enabled) { + assertThrows(ReceiptCheckErrException.class, trace::check); + } else { + // Preserve the historical receipt check before the proposal activates. + trace.check(); + } + trace(validator, contractResult.REVERT).check(); + } + + private ProgramResult executeRevert(boolean timeout) throws Exception { + ProgramInvoke invoke = mock(ProgramInvoke.class); + when(invoke.getEnergyLimit()).thenReturn(ENERGY_LIMIT); + when(invoke.getVmShouldEndInUs()).thenReturn(Long.MAX_VALUE); + when(invoke.getContractAddress()).thenReturn(new DataWord(1)); + // PUSH1 0 PUSH1 0 REVERT. Only the post-execution CPU check is forced to time out. + Program program = new Program(new byte[]{0x60, 0, 0x60, 0, (byte) Op.REVERT}, new byte[21], + invoke, mock(InternalTransaction.class)) { + @Override + public void checkCPUTimeLimit(String opName) { + if (timeout && opName.endsWith("(TX_LAST_OP)")) { + throw Program.Exception.notEnoughTime(opName); + } + } + }; + program.setRootTransactionId(new byte[32]); + VMActuator actuator = new VMActuator(false); + actuator.setTrxType(contractType == ContractType.CreateSmartContract + ? TrxType.TRX_CONTRACT_CREATION_TYPE : TrxType.TRX_CONTRACT_CALL_TYPE); + ReflectionTestUtils.setField(actuator, "program", program); + ReflectionTestUtils.setField(actuator, "rootRepository", mock(Repository.class)); + TransactionContext context = new TransactionContext(null, null, stores, false, false); + actuator.execute(context); + ProgramResult result = context.getProgramResult(); + ReflectionTestUtils.invokeMethod(new RuntimeImpl(), "setResultCode", result); + return result; + } + + private TransactionTrace trace(ProgramResult result, contractResult expected) { + Transaction transaction = Transaction.newBuilder() + .setRawData(Transaction.raw.newBuilder().addContract( + Transaction.Contract.newBuilder().setType(contractType))) + .addRet(Transaction.Result.newBuilder().setContractRet(expected)) + .build(); + TransactionTrace trace = new TransactionTrace(new TransactionCapsule(transaction), stores, + new RuntimeImpl()); + trace.init(null); + trace.getTransactionContext().setProgramResult(result); + trace.setBill(result.getEnergyUsed()); + trace.setResult(); + return trace; + } +}