From 0af15279c422f3f26b14461b44d93bc57c409ded Mon Sep 17 00:00:00 2001 From: Asuka Date: Mon, 1 Jun 2026 16:55:04 +0800 Subject: [PATCH 1/2] feat(vm): check CREATE2 max depth under Osaka --- .../org/tron/core/vm/program/Program.java | 3 +- .../common/runtime/vm/OperationsTest.java | 66 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) 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 3ed968e1af..aeaf1dacfa 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 @@ -1621,7 +1621,8 @@ public void createContract2(DataWord value, DataWord memStart, DataWord memSize, } byte[] senderAddress; - if (VMConfig.allowTvmCompatibleEvm() && getCallDeep() == MAX_DEPTH) { + if ((VMConfig.allowTvmCompatibleEvm() || VMConfig.allowTvmOsaka()) + && getCallDeep() == MAX_DEPTH) { stackPushZero(); return; } diff --git a/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java b/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java index 651248bd9e..a1627f4f2e 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java @@ -788,6 +788,72 @@ Op.CALL, new DataWord(10000), VMConfig.initAllowTvmSelfdestructRestriction(0); } + @Test + public void testCreate2MaxDepthWithOsakaOnly() throws ContractValidateException { + boolean allowTvmCompatibleEvm = VMConfig.allowTvmCompatibleEvm(); + boolean allowTvmOsaka = VMConfig.allowTvmOsaka(); + VMConfig.initAllowTvmCompatibleEvm(0); + VMConfig.initAllowTvmOsaka(1); + try { + invoke = new ProgramInvokeMockImpl() { + @Override + public int getCallDeep() { + return 64; + } + }; + Protocol.Transaction trx = Protocol.Transaction.getDefaultInstance(); + InternalTransaction interTrx = + new InternalTransaction(trx, InternalTransaction.TrxType.TRX_UNKNOWN_TYPE); + program = new Program(new byte[0], new byte[0], invoke, interTrx); + program.setRootTransactionId(new byte[32]); + + program.createContract2(DataWord.ZERO(), DataWord.ZERO(), DataWord.ZERO(), DataWord.ZERO()); + + Assert.assertEquals(DataWord.ZERO(), program.getStack().pop()); + Assert.assertTrue(program.getResult().getInternalTransactions().isEmpty()); + } finally { + VMConfig.initAllowTvmCompatibleEvm(allowTvmCompatibleEvm ? 1 : 0); + VMConfig.initAllowTvmOsaka(allowTvmOsaka ? 1 : 0); + } + } + + @Test + public void testCreate2MaxDepthWithNeitherFlag() throws ContractValidateException { + boolean allowTvmCompatibleEvm = VMConfig.allowTvmCompatibleEvm(); + boolean allowTvmOsaka = VMConfig.allowTvmOsaka(); + VMConfig.initAllowTvmCompatibleEvm(0); + VMConfig.initAllowTvmOsaka(0); + try { + byte[] contractAddr = Hex.decode("41471fd3ad3e9eeadeec4608b92d16ce6b500704cc"); + invoke = new ProgramInvokeMockImpl(StoreFactory.getInstance(), new byte[0], contractAddr) { + @Override + public int getCallDeep() { + return 64; + } + + @Override + public boolean byTestingSuite() { + return true; + } + }; + program = new Program(null, null, invoke, + new InternalTransaction(Protocol.Transaction.getDefaultInstance(), + InternalTransaction.TrxType.TRX_UNKNOWN_TYPE)); + program.setRootTransactionId(new byte[32]); + + program.createContract2(DataWord.ZERO(), DataWord.ZERO(), DataWord.ZERO(), DataWord.ZERO()); + + // With neither flag enabled the MAX_DEPTH short-circuit must not fire: CREATE2 + // proceeds, records an internal transaction and pushes the new contract + // address (not 0), unlike the Osaka/CompatibleEvm guarded path above. + Assert.assertFalse(program.getResult().getInternalTransactions().isEmpty()); + Assert.assertFalse(program.getStack().pop().isZero()); + } finally { + VMConfig.initAllowTvmCompatibleEvm(allowTvmCompatibleEvm ? 1 : 0); + VMConfig.initAllowTvmOsaka(allowTvmOsaka ? 1 : 0); + } + } + // TIP-854 outer-frame containment: a CALL to validateMultiSign or // batchValidateSign with malformed calldata must (a) push 0 onto the outer // stack, (b) leave the outer frame free of any propagated exception, and From b9a8399928a089e0073808e769e18363e4e42754 Mon Sep 17 00:00:00 2001 From: Asuka Date: Thu, 11 Jun 2026 19:25:15 +0800 Subject: [PATCH 2/2] feat(vm): clear residual logs on exception escape paths under Osaka Mirror the in-try failure path: failed txs must not leak event logs into TransactionInfo/Bloom. Gated on Osaka to preserve replay. --- .../org/tron/core/actuator/VMActuator.java | 10 +++ .../common/runtime/VMActuatorMockTest.java | 88 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java diff --git a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java index 1b0e8a6637..f604013325 100644 --- a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java @@ -267,6 +267,7 @@ public void execute(Object object) throws ContractExeException { result = program.getResult(); result.setException(e); result.rejectInternalTransactions(); + clearExceptionResult(result); result.setRuntimeError(result.getException().getMessage()); logger.info("JVMStackOverFlowException: {}", result.getException().getMessage()); } catch (OutOfTimeException e) { @@ -274,6 +275,7 @@ public void execute(Object object) throws ContractExeException { result = program.getResult(); result.setException(e); result.rejectInternalTransactions(); + clearExceptionResult(result); result.setRuntimeError(result.getException().getMessage()); logger.info("timeout: {}", result.getException().getMessage()); } catch (Throwable e) { @@ -282,6 +284,7 @@ public void execute(Object object) throws ContractExeException { } result = program.getResult(); result.rejectInternalTransactions(); + clearExceptionResult(result); if (Objects.isNull(result.getException())) { logger.error(e.getMessage(), e); result.setException(new RuntimeException("Unknown Throwable")); @@ -310,6 +313,13 @@ public void execute(Object object) throws ContractExeException { } + private void clearExceptionResult(ProgramResult result) { + if (VMConfig.allowTvmOsaka()) { + result.getDeleteAccounts().clear(); + result.getLogInfoList().clear(); + } + } + private void create() throws ContractValidateException { if (!rootRepository.getDynamicPropertiesStore().supportVM()) { diff --git a/framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java b/framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java new file mode 100644 index 0000000000..ab147f57a7 --- /dev/null +++ b/framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java @@ -0,0 +1,88 @@ +package org.tron.common.runtime; + +import static org.mockito.ArgumentMatchers.any; + +import java.lang.reflect.Field; +import java.util.Collections; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.tron.common.runtime.vm.DataWord; +import org.tron.common.runtime.vm.LogInfo; +import org.tron.core.actuator.VMActuator; +import org.tron.core.db.TransactionContext; +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.Program; + +public class VMActuatorMockTest { + + @BeforeClass + public static void init() { + // warm up the registry so VM.play(..., OperationRegistry.getTable()) arg eval is safe + OperationRegistry.init(); + } + + private void runCatchPathTest(Throwable thrownByVm, boolean osakaOn, int expectedSize) + throws Exception { + boolean prevOsaka = VMConfig.allowTvmOsaka(); + VMConfig.initAllowTvmOsaka(osakaOn ? 1 : 0); + try (MockedStatic vmMock = Mockito.mockStatic(VM.class)) { + Program program = Mockito.mock(Program.class); + ProgramResult result = new ProgramResult(); + result.addLogInfo(new LogInfo(new byte[20], Collections.emptyList(), new byte[0])); + result.addDeleteAccount(new DataWord(1)); + Mockito.when(program.getResult()).thenReturn(result); + + vmMock.when(() -> VM.play(any(), any())).thenThrow(thrownByVm); + + VMActuator actuator = new VMActuator(false); + Field f = VMActuator.class.getDeclaredField("program"); + f.setAccessible(true); + f.set(actuator, program); + + TransactionContext context = Mockito.mock(TransactionContext.class); + Mockito.when(context.getProgramResult()).thenReturn(new ProgramResult()); + + actuator.execute(context); + + Assert.assertEquals(expectedSize, result.getLogInfoList().size()); + Assert.assertEquals(expectedSize, result.getDeleteAccounts().size()); + } finally { + VMConfig.initAllowTvmOsaka(prevOsaka ? 1 : 0); + } + } + + @Test + public void osakaClearsLogOnOutOfTime() throws Exception { + runCatchPathTest(new Program.OutOfTimeException("timeout"), true, 0); + } + + @Test + public void osakaClearsLogOnJvmStackOverflow() throws Exception { + runCatchPathTest(new Program.JVMStackOverFlowException(), true, 0); + } + + @Test + public void osakaClearsLogOnThrowable() throws Exception { + runCatchPathTest(new RuntimeException("boom"), true, 0); + } + + @Test + public void preOsakaKeepsLogOnOutOfTime() throws Exception { + runCatchPathTest(new Program.OutOfTimeException("timeout"), false, 1); + } + + @Test + public void preOsakaKeepsLogOnJvmStackOverflow() throws Exception { + runCatchPathTest(new Program.JVMStackOverFlowException(), false, 1); + } + + @Test + public void preOsakaKeepsLogOnThrowable() throws Exception { + runCatchPathTest(new RuntimeException("boom"), false, 1); + } +}