From c854ccbbb9086e2cafec69c9239de50cbec6e148 Mon Sep 17 00:00:00 2001 From: Asuka Date: Thu, 12 Nov 2026 11:24:52 +0800 Subject: [PATCH 1/2] feat(tvm): optimize execution result classification Prioritize recognized exception result codes over REVERT when TVM resource optimization is enabled, so post-REVERT CPU timeouts can trigger retry and receipt validation. Preserve historical REVERT priority before activation. Cover exception mappings and post-REVERT timeouts for contract calls and creation. --- .../org/tron/common/runtime/RuntimeImpl.java | 9 +- .../common/runtime/RuntimeImplMockTest.java | 112 +++++++---- .../runtime/RuntimeImplRevertTimeoutTest.java | 181 ++++++++++++++++++ 3 files changed, 261 insertions(+), 41 deletions(-) create mode 100644 framework/src/test/java/org/tron/common/runtime/RuntimeImplRevertTimeoutTest.java 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; + } +} From 24eb0d239efe9e393b725620fa686998b259c10b Mon Sep 17 00:00:00 2001 From: Asuka Date: Thu, 12 Nov 2026 17:04:31 +0800 Subject: [PATCH 2/2] feat(tvm): optimize nested creation revert handling Preserve constructor REVERT data beginning with 0xEF when TVM resource optimization is active. Retain London code rejection for normal returns and historical behavior before activation. Cover CREATE and CREATE2 activation combinations, remaining Energy, caller-visible returndata, child rollback, exception precedence, deadline handling and the existing Osaka return-buffer boundary. --- .../org/tron/core/vm/program/Program.java | 5 +- .../vm/program/ProgramCreateRevertTest.java | 299 ++++++++++++++++++ 2 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 actuator/src/test/java/org/tron/core/vm/program/ProgramCreateRevertTest.java 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))); + } +}