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 d785951027..e2cb3c3f4d 100644 --- a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java @@ -18,6 +18,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.bouncycastle.util.encoders.Hex; +import org.tron.common.crypto.Hash; import org.tron.common.logsfilter.trigger.ContractTrigger; import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.InternalTransaction; @@ -25,6 +26,7 @@ import org.tron.common.runtime.InternalTransaction.TrxType; import org.tron.common.runtime.ProgramResult; import org.tron.common.runtime.vm.DataWord; +import org.tron.common.utils.FastByteComparisons; import org.tron.common.utils.StorageUtils; import org.tron.common.utils.StringUtil; import org.tron.common.utils.WalletUtil; @@ -189,7 +191,8 @@ public void execute(Object object) throws ContractExeException { throw e; } - VM.play(program, OperationRegistry.getTable()); + // Prepare the table once for this execution and all nested calls. + VM.play(program, OperationRegistry.beginExecution(isConstantCall)); result = program.getResult(); if (VMConfig.allowEnergyAdjustment()) { @@ -217,6 +220,10 @@ public void execute(Object object) throws ContractExeException { } else { result.spendEnergy(saveCodeEnergy); if (VMConfig.allowTvmConstantinople()) { + SmartContract contract = ContractCapsule.getSmartContractFromTransaction(trx).getNewContract(); + if (FastByteComparisons.isEqual(contract.getCodeHash().toByteArray(), Hash.sha3(code))) { + MUtil.checkCPUTimeForCodeHash(); + } rootRepository.saveCode(program.getContractAddress().getNoLeadZeroesData(), code); } } @@ -293,6 +300,9 @@ public void execute(Object object) throws ContractExeException { result.setRuntimeError(result.getException().getMessage()); } logger.info("runtime result is :{}", result.getException().getMessage()); + } finally { + // Also release constant-call state on early return or failure. + OperationRegistry.endExecution(isConstantCall); } //use program returned fill context context.setProgramResult(result); diff --git a/actuator/src/main/java/org/tron/core/vm/OperationRegistry.java b/actuator/src/main/java/org/tron/core/vm/OperationRegistry.java index 8c078e843a..6084f42680 100644 --- a/actuator/src/main/java/org/tron/core/vm/OperationRegistry.java +++ b/actuator/src/main/java/org/tron/core/vm/OperationRegistry.java @@ -30,6 +30,9 @@ public enum Version { tableMap.put(Version.TRON_V1_5, newTronV15OperationSet()); } + // Private table for one constant-call execution. + private static final ThreadLocal localTable = new ThreadLocal<>(); + public static JumpTable newTronV10OperationSet() { JumpTable table = newBaseOperationSet(); appendTransferTrc10Operations(table); @@ -74,11 +77,48 @@ public static JumpTable newTronV15OperationSet() { // Just for warming up class to avoid out_of_time public static void init() {} - public static JumpTable getTable() { - // always get the table which has the newest version - JumpTable table = tableMap.get(Version.TRON_V1_5); + public static JumpTable beginExecution(boolean isConstantCall) { + JumpTable table; + if (isConstantCall) { + // Do not reuse state left by a pooled RPC worker. + localTable.remove(); + // Keep constant-call adjustments away from the shared consensus table. + table = newTronV15OperationSet(); + } else { + table = tableMap.get(Version.TRON_V1_5); + } + + // Apply configuration-dependent changes once at the top level. + adjustTable(table); + + if (isConstantCall) { + localTable.set(table); + } + + return table; + } + + public static JumpTable getTable(boolean isConstantCall) { + if (!isConstantCall) { + return tableMap.get(Version.TRON_V1_5); + } - // next make the corresponding changes, exclude activating opcode + // Nested constant calls reuse the table prepared by the top level. + JumpTable table = localTable.get(); + if (table == null) { + throw new IllegalStateException("JumpTable execution context is not initialized"); + } + return table; + } + + public static void endExecution(boolean isConstantCall) { + if (isConstantCall) { + localTable.remove(); + } + } + + private static void adjustTable(JumpTable table) { + // Make the corresponding changes, excluding opcode activation. if (VMConfig.allowHigherLimitForMaxCpuTimeOfOneTx()) { adjustMemOperations(table); } @@ -94,8 +134,6 @@ public static JumpTable getTable() { if (VMConfig.allowTvmOsaka()) { adjustVoteWitnessCost(table); } - - return table; } public static JumpTable newBaseOperationSet() { diff --git a/actuator/src/main/java/org/tron/core/vm/nativecontract/CancelAllUnfreezeV2Processor.java b/actuator/src/main/java/org/tron/core/vm/nativecontract/CancelAllUnfreezeV2Processor.java index ec1f436320..6ee8b1245c 100644 --- a/actuator/src/main/java/org/tron/core/vm/nativecontract/CancelAllUnfreezeV2Processor.java +++ b/actuator/src/main/java/org/tron/core/vm/nativecontract/CancelAllUnfreezeV2Processor.java @@ -19,6 +19,7 @@ import org.tron.core.vm.VMConstant; import org.tron.core.vm.nativecontract.param.CancelAllUnfreezeV2Param; import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.utils.MUtil; import org.tron.protos.Protocol; @Slf4j(topic = "VMProcessor") @@ -39,6 +40,10 @@ public void validate(CancelAllUnfreezeV2Param param, Repository repo) throws Con throw new ContractValidateException( ACCOUNT_EXCEPTION_STR + readableOwnerAddress + NOT_EXIST_STR); } + + if (accountCapsule.hasInvalidDelegatedV2()) { + MUtil.checkCPUTimeForInvalidDelegatedV2Balance(); + } } public Map execute(CancelAllUnfreezeV2Param param, Repository repo) throws ContractExeException { diff --git a/actuator/src/main/java/org/tron/core/vm/nativecontract/FreezeBalanceV2Processor.java b/actuator/src/main/java/org/tron/core/vm/nativecontract/FreezeBalanceV2Processor.java index e7e932194e..96c6e936b3 100644 --- a/actuator/src/main/java/org/tron/core/vm/nativecontract/FreezeBalanceV2Processor.java +++ b/actuator/src/main/java/org/tron/core/vm/nativecontract/FreezeBalanceV2Processor.java @@ -14,6 +14,7 @@ import org.tron.core.store.DynamicPropertiesStore; import org.tron.core.vm.nativecontract.param.FreezeBalanceV2Param; import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.utils.MUtil; @Slf4j(topic = "VMProcessor") public class FreezeBalanceV2Processor { @@ -63,6 +64,10 @@ public void validate(FreezeBalanceV2Param param, Repository repo) throws Contrac "Unknown ResourceCode, valid ResourceCode[BANDWIDTH、ENERGY]"); } } + + if (repo.isSelfDestructed(ownerAddress)) { + MUtil.checkCPUTimeForFreezeV2AfterSelfDestruct(); + } } public void execute(FreezeBalanceV2Param param, Repository repo) { diff --git a/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceV2Processor.java b/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceV2Processor.java index af2cbf63a4..73ffb5f294 100644 --- a/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceV2Processor.java +++ b/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceV2Processor.java @@ -23,6 +23,7 @@ import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.nativecontract.param.UnfreezeBalanceV2Param; import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.utils.MUtil; import org.tron.core.vm.utils.VoteRewardUtil; import org.tron.protos.Protocol; import org.tron.protos.contract.Common; @@ -86,6 +87,10 @@ public void validate(UnfreezeBalanceV2Param param, Repository repo) throw new ContractValidateException( "Invalid unfreeze_balance, [" + param.getUnfreezeBalance() + "] is invalid"); } + + if (accountCapsule.hasInvalidDelegatedV2()) { + MUtil.checkCPUTimeForInvalidDelegatedV2Balance(); + } } private boolean checkUnfreezeBalance( diff --git a/actuator/src/main/java/org/tron/core/vm/nativecontract/WithdrawExpireUnfreezeProcessor.java b/actuator/src/main/java/org/tron/core/vm/nativecontract/WithdrawExpireUnfreezeProcessor.java index 0bcdb10d46..982031aa67 100644 --- a/actuator/src/main/java/org/tron/core/vm/nativecontract/WithdrawExpireUnfreezeProcessor.java +++ b/actuator/src/main/java/org/tron/core/vm/nativecontract/WithdrawExpireUnfreezeProcessor.java @@ -17,6 +17,7 @@ import org.tron.core.store.DynamicPropertiesStore; import org.tron.core.vm.nativecontract.param.WithdrawExpireUnfreezeParam; import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.utils.MUtil; import org.tron.protos.Protocol; @Slf4j(topic = "VMProcessor") @@ -52,6 +53,10 @@ public void validate(WithdrawExpireUnfreezeParam param, Repository repo) throws logger.debug(e.getMessage(), e); throw new ContractValidateException(e.getMessage()); } + + if (accountCapsule.hasInvalidDelegatedV2()) { + MUtil.checkCPUTimeForInvalidDelegatedV2Balance(); + } } private long getTotalWithdrawUnfreeze(List unfrozenV2List, long now) { diff --git a/actuator/src/main/java/org/tron/core/vm/program/ContractState.java b/actuator/src/main/java/org/tron/core/vm/program/ContractState.java index c6347b9a07..30ec0d24f3 100644 --- a/actuator/src/main/java/org/tron/core/vm/program/ContractState.java +++ b/actuator/src/main/java/org/tron/core/vm/program/ContractState.java @@ -131,6 +131,16 @@ public boolean isNewContract(byte[] address) { return repository.isNewContract(address); } + @Override + public void markSelfDestruct(byte[] address) { + repository.markSelfDestruct(address); + } + + @Override + public boolean isSelfDestructed(byte[] address) { + return repository.isSelfDestructed(address); + } + @Override public void updateAccount(byte[] address, AccountCapsule accountCapsule) { repository.updateAccount(address, accountCapsule); 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 590859a9fe..2be1eb8070 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 @@ -512,12 +512,18 @@ public void suicide(DataWord obtainerAddress) { internalTx.setValue(internalTx.getValue() + expireUnfrozenBalance); } } + + getContractState().markSelfDestruct(owner); getResult().addDeleteAccount(this.getContractAddress()); } public void suicide2(DataWord obtainerAddress) { - byte[] owner = getContextAddress(); + + if (getContractState().isSelfDestructed(obtainerAddress.toTronAddress())) { + MUtil.checkCPUTimeForSelfDestructedBeneficiary(); + } + boolean isNewContract = getContractState().isNewContract(owner); if (isNewContract) { suicide(obtainerAddress); @@ -540,6 +546,7 @@ public void suicide2(DataWord obtainerAddress) { "suicide", nonce, getContractState().getAccount(owner).getAssetMapV2()); if (FastByteComparisons.isEqual(owner, obtainer)) { + getContractState().markSelfDestruct(owner); return; } @@ -579,6 +586,8 @@ public void suicide2(DataWord obtainerAddress) { internalTx.setValue(internalTx.getValue() + expireUnfrozenBalance); } } + + getContractState().markSelfDestruct(owner); } public Repository getContractState() { @@ -914,7 +923,8 @@ this, new DataWord(newAddress), getContractAddress(), value, DataWord.ZERO(), if (VMConfig.allowTvmCompatibleEvm()) { program.setContractVersion(getContractVersion()); } - VM.play(program, OperationRegistry.getTable()); + // Reuse the table prepared by the top-level execution. + VM.play(program, OperationRegistry.getTable(isConstantCall())); createResult = program.getResult(); getTrace().merge(program.getTrace()); // always commit nonce @@ -1146,7 +1156,8 @@ this, new DataWord(contextAddress), program.setContractVersion(invoke.getDeposit() .getContract(codeAddress).getContractVersion()); } - VM.play(program, OperationRegistry.getTable()); + // Reuse the table prepared by the top-level execution. + VM.play(program, OperationRegistry.getTable(isConstantCall())); callResult = program.getResult(); getTrace().merge(program.getTrace()); diff --git a/actuator/src/main/java/org/tron/core/vm/repository/Repository.java b/actuator/src/main/java/org/tron/core/vm/repository/Repository.java index 8f91d59d0b..ab86328c42 100644 --- a/actuator/src/main/java/org/tron/core/vm/repository/Repository.java +++ b/actuator/src/main/java/org/tron/core/vm/repository/Repository.java @@ -59,6 +59,10 @@ public interface Repository { boolean isNewContract(byte[] address); + void markSelfDestruct(byte[] address); + + boolean isSelfDestructed(byte[] address); + void updateAccount(byte[] address, AccountCapsule accountCapsule); void updateDynamicProperty(byte[] word, BytesCapsule bytesCapsule); diff --git a/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java index 62e7ce6ec0..7801a18798 100644 --- a/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java +++ b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java @@ -139,6 +139,7 @@ public class RepositoryImpl implements Repository { private final HashMap> delegatedResourceAccountIndexCache = new HashMap<>(); private final HashBasedTable> transientStorage = HashBasedTable.create(); private final HashSet newContractCache = new HashSet<>(); + private final HashSet selfDestructCache = new HashSet<>(); public static void removeLruCache(byte[] address) { } @@ -572,6 +573,29 @@ public boolean isNewContract(byte[] address) { } } + @Override + public void markSelfDestruct(byte[] address) { + selfDestructCache.add(Key.create(address)); + } + + @Override + public boolean isSelfDestructed(byte[] address) { + Key key = Key.create(address); + if (selfDestructCache.contains(key)) { + return true; + } + + if (parent != null) { + boolean isSelfDestructed = parent.isSelfDestructed(address); + if (isSelfDestructed) { + selfDestructCache.add(key); + } + return isSelfDestructed; + } else { + return false; + } + } + @Override public void updateAccount(byte[] address, AccountCapsule accountCapsule) { accountCache.put(Key.create(address), @@ -780,6 +804,7 @@ public void commit() { commitDelegatedResourceAccountIndexCache(repository); commitTransientStorage(repository); commitNewContractCache(repository); + commitSelfDestructCache(repository); } @Override @@ -1142,6 +1167,12 @@ public void commitNewContractCache(Repository deposit) { } } + private void commitSelfDestructCache(Repository deposit) { + if (deposit != null) { + selfDestructCache.forEach(key -> deposit.markSelfDestruct(key.getData())); + } + } + /** * Get the block id from the number. */ diff --git a/actuator/src/main/java/org/tron/core/vm/utils/MUtil.java b/actuator/src/main/java/org/tron/core/vm/utils/MUtil.java index e07360e686..4c78d025ef 100644 --- a/actuator/src/main/java/org/tron/core/vm/utils/MUtil.java +++ b/actuator/src/main/java/org/tron/core/vm/utils/MUtil.java @@ -76,4 +76,29 @@ public static void checkCPUTimeForModExp() { throw new OutOfTimeException("CPU timeout for modExp executing"); } } + + public static void checkCPUTimeForFreezeV2AfterSelfDestruct() { + if (ForkController.instance().pass(Parameter.ForkBlockVersionEnum.VERSION_4_8_2_2)) { + throw new OutOfTimeException("CPU timeout for FreezeBalanceV2 after SELFDESTRUCT"); + } + } + + public static void checkCPUTimeForSelfDestructedBeneficiary() { + if (ForkController.instance().pass(Parameter.ForkBlockVersionEnum.VERSION_4_8_2_2)) { + throw new OutOfTimeException( + "CPU timeout for SELFDESTRUCT with selfdestructed beneficiary"); + } + } + + public static void checkCPUTimeForInvalidDelegatedV2Balance() { + if (ForkController.instance().pass(Parameter.ForkBlockVersionEnum.VERSION_4_8_2_2)) { + throw new OutOfTimeException("CPU timeout for invalid delegated V2 balance"); + } + } + + public static void checkCPUTimeForCodeHash() { + if (ForkController.instance().pass(Parameter.ForkBlockVersionEnum.VERSION_4_8_2_2)) { + throw new OutOfTimeException("CPU timeout for invalid code hash"); + } + } } diff --git a/chainbase/src/main/java/org/tron/core/capsule/AccountCapsule.java b/chainbase/src/main/java/org/tron/core/capsule/AccountCapsule.java index 1af7b55c8b..026dce74ec 100644 --- a/chainbase/src/main/java/org/tron/core/capsule/AccountCapsule.java +++ b/chainbase/src/main/java/org/tron/core/capsule/AccountCapsule.java @@ -1336,6 +1336,10 @@ public void clearDelegatedResource() { this.account = builder.build(); } + public boolean hasInvalidDelegatedV2() { + return getDelegatedFrozenV2BalanceForBandwidth() < 0 || getDelegatedFrozenV2BalanceForEnergy() < 0; + } + public void importAsset(byte[] key) { this.account = AssetUtil.importAsset(this.account, key); } diff --git a/common/src/main/java/org/tron/core/config/Parameter.java b/common/src/main/java/org/tron/core/config/Parameter.java index 233f1d9ef7..0f9402641e 100644 --- a/common/src/main/java/org/tron/core/config/Parameter.java +++ b/common/src/main/java/org/tron/core/config/Parameter.java @@ -30,7 +30,8 @@ public enum ForkBlockVersionEnum { VERSION_4_8_0_1(33, 1596780000000L, 70), VERSION_4_8_1(34, 1596780000000L, 80), VERSION_4_8_1_1(35, 1596780000000L, 70), - VERSION_4_8_2(36, 1596780000000L, 80); + VERSION_4_8_2(36, 1596780000000L, 80), + VERSION_4_8_2_2(37, 1596780000000L, 70); // if add a version, modify BLOCK_VERSION simultaneously @Getter @@ -79,7 +80,7 @@ public class ChainConstant { public static final int SINGLE_REPEAT = 1; public static final int BLOCK_FILLED_SLOTS_NUMBER = 128; public static final int MAX_FROZEN_NUMBER = 1; - public static final int BLOCK_VERSION = 36; + public static final int BLOCK_VERSION = 37; public static final long FROZEN_PERIOD = 86_400_000L; public static final long DELEGATE_PERIOD = 3 * 86_400_000L; public static final long TRX_PRECISION = 1000_000L; diff --git a/framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java b/framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java index ab147f57a7..4a5268b534 100644 --- a/framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java +++ b/framework/src/test/java/org/tron/common/runtime/VMActuatorMockTest.java @@ -1,6 +1,8 @@ package org.tron.common.runtime; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.same; import java.lang.reflect.Field; import java.util.Collections; @@ -13,21 +15,85 @@ import org.tron.common.runtime.vm.LogInfo; import org.tron.core.actuator.VMActuator; import org.tron.core.db.TransactionContext; +import org.tron.core.vm.JumpTable; 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; +import org.tron.core.vm.repository.Repository; public class VMActuatorMockTest { @BeforeClass public static void init() { - // warm up the registry so VM.play(..., OperationRegistry.getTable()) arg eval is safe + // Warm up the registry before VM execution timing starts. OperationRegistry.init(); } + @Test + public void constantCallClearsJumpTableOnEarlyReturn() throws Exception { + try (MockedStatic vmMock = Mockito.mockStatic(VM.class)) { + Program program = Mockito.mock(Program.class); + Mockito.when(program.getResult()).thenReturn(new ProgramResult()); + + VMActuator actuator = new VMActuator(true); + 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); + + // The constant call must receive a private table, not the shared consensus one. + JumpTable shared = OperationRegistry.getTable(false); + vmMock.verify(() -> VM.play(any(), argThat(table -> table != shared))); + + try { + OperationRegistry.getTable(true); + Assert.fail("constant-call JumpTable must be cleared after execution"); + } catch (IllegalStateException expected) { + // expected + } + } finally { + OperationRegistry.endExecution(true); + } + } + + @Test + public void nonConstantCallUsesSharedJumpTable() throws Exception { + try (MockedStatic vmMock = Mockito.mockStatic(VM.class)) { + Program program = Mockito.mock(Program.class); + Mockito.when(program.getResult()).thenReturn(new ProgramResult()); + + VMActuator actuator = new VMActuator(false); + Field f = VMActuator.class.getDeclaredField("program"); + f.setAccessible(true); + f.set(actuator, program); + + Field repositoryField = VMActuator.class.getDeclaredField("rootRepository"); + repositoryField.setAccessible(true); + repositoryField.set(actuator, Mockito.mock(Repository.class)); + + TransactionContext context = Mockito.mock(TransactionContext.class); + Mockito.when(context.getProgramResult()).thenReturn(new ProgramResult()); + + actuator.execute(context); + + // The non-constant call must receive the shared consensus table itself. + JumpTable shared = OperationRegistry.getTable(false); + vmMock.verify(() -> VM.play(any(), same(shared))); + } + } + private void runCatchPathTest(Throwable thrownByVm, boolean osakaOn, int expectedSize) throws Exception { + runCatchPathTest(thrownByVm, osakaOn, expectedSize, false); + } + + private void runCatchPathTest(Throwable thrownByVm, boolean osakaOn, int expectedSize, + boolean constantCall) throws Exception { boolean prevOsaka = VMConfig.allowTvmOsaka(); VMConfig.initAllowTvmOsaka(osakaOn ? 1 : 0); try (MockedStatic vmMock = Mockito.mockStatic(VM.class)) { @@ -39,7 +105,7 @@ private void runCatchPathTest(Throwable thrownByVm, boolean osakaOn, int expecte vmMock.when(() -> VM.play(any(), any())).thenThrow(thrownByVm); - VMActuator actuator = new VMActuator(false); + VMActuator actuator = new VMActuator(constantCall); Field f = VMActuator.class.getDeclaredField("program"); f.setAccessible(true); f.set(actuator, program); @@ -56,6 +122,22 @@ private void runCatchPathTest(Throwable thrownByVm, boolean osakaOn, int expecte } } + @Test + public void constantCallClearsJumpTableOnFailure() throws Exception { + try { + runCatchPathTest(new RuntimeException("boom"), false, 1, true); + + try { + OperationRegistry.getTable(true); + Assert.fail("constant-call JumpTable must be cleared after failure"); + } catch (IllegalStateException expected) { + // expected + } + } finally { + OperationRegistry.endExecution(true); + } + } + @Test public void osakaClearsLogOnOutOfTime() throws Exception { runCatchPathTest(new Program.OutOfTimeException("timeout"), true, 0); 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 a1627f4f2e..a161eeae39 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 @@ -3,6 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.tron.core.config.Parameter.ChainConstant.FROZEN_PERIOD; +import static org.tron.core.config.Parameter.ForkBlockVersionEnum.VERSION_4_8_2_2; import java.util.List; import java.util.Locale; @@ -16,6 +17,7 @@ import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; +import org.mockito.MockedStatic; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; @@ -24,6 +26,7 @@ import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.InternalTransaction; import org.tron.common.utils.DecodeUtil; +import org.tron.common.utils.ForkController; import org.tron.core.Constant; import org.tron.core.Wallet; import org.tron.core.capsule.AccountCapsule; @@ -42,6 +45,7 @@ import org.tron.core.vm.config.ConfigLoader; import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.program.Program; +import org.tron.core.vm.program.Program.OutOfTimeException; import org.tron.core.vm.program.invoke.ProgramInvokeMockImpl; import org.tron.core.vm.repository.Repository; import org.tron.protos.Protocol; @@ -51,7 +55,7 @@ public class OperationsTest extends BaseTest { private ProgramInvokeMockImpl invoke; private Program program; - private final JumpTable jumpTable = OperationRegistry.getTable(); + private final JumpTable jumpTable = OperationRegistry.beginExecution(false); @Autowired private Wallet wallet; @@ -1182,6 +1186,7 @@ public void testSuicideAction() throws ContractValidateException { program.suicide(new DataWord( dbManager.getAccountStore().getBlackhole().getAddress().toByteArray())); + Assert.assertTrue(program.getContractState().isSelfDestructed(program.getContextAddress())); DecodeUtil.addressPreFixByte = prePrefixByte; VMConfig.initAllowEnergyAdjustment(0); @@ -1240,6 +1245,7 @@ public void testSuicideAction2() throws ContractValidateException { OperationActions.suicideAction2(program); Assert.assertEquals(1, program.getResult().getDeleteAccounts().size()); + Assert.assertTrue(program.getContractState().isSelfDestructed(contractAddr)); invoke = new ProgramInvokeMockImpl(StoreFactory.getInstance(), new byte[0], contractAddr); @@ -1256,6 +1262,7 @@ public void testSuicideAction2() throws ContractValidateException { dbManager.getAccountStore().getBlackhole().getAddress().toByteArray())); Assert.assertEquals(0, spyProgram.getResult().getDeleteAccounts().size()); + Assert.assertTrue(spyProgram.getContractState().isSelfDestructed(contractAddr)); DecodeUtil.addressPreFixByte = prePrefixByte; VMConfig.initAllowEnergyAdjustment(0); @@ -1266,6 +1273,31 @@ public void testSuicideAction2() throws ContractValidateException { VMConfig.initAllowTvmVote(0); } + @Test + public void testSuicide2RejectsSelfDestructedBeneficiaryAfterFork() + throws ContractValidateException { + byte[] contractAddr = Hex.decode("41471fd3ad3e9eeadeec4608b92d16ce6b500704cc"); + byte[] beneficiary = Hex.decode("411111111111111111111111111111111111111111"); + invoke = new ProgramInvokeMockImpl(StoreFactory.getInstance(), new byte[0], contractAddr); + program = new Program(null, null, invoke, + new InternalTransaction( + Protocol.Transaction.getDefaultInstance(), + InternalTransaction.TrxType.TRX_UNKNOWN_TYPE)); + program.getContractState().markSelfDestruct(beneficiary); + + ForkController forkController = Mockito.mock(ForkController.class); + try (MockedStatic fork = Mockito.mockStatic(ForkController.class)) { + fork.when(ForkController::instance).thenReturn(forkController); + Mockito.when(forkController.pass(VERSION_4_8_2_2)).thenReturn(true); + + OutOfTimeException exception = Assert.assertThrows(OutOfTimeException.class, + () -> program.suicide2(new DataWord(beneficiary))); + Assert.assertEquals( + "CPU timeout for SELFDESTRUCT with selfdestructed beneficiary", + exception.getMessage()); + } + } + @Test public void testVoteWitnessCost() throws ContractValidateException { // Build stack environment, the stack from top to bottom is 0x00, 0x80, 0x00, 0x80 diff --git a/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java b/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java index 2c7aa23803..9be62573bc 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/VoteWitnessCost3Test.java @@ -217,7 +217,7 @@ public void testWitnessArrayLargerThanAmountArray() { @Test public void testOperationRegistryWithoutOsaka() { VMConfig.initAllowTvmOsaka(0); - JumpTable table = OperationRegistry.getTable(); + JumpTable table = OperationRegistry.beginExecution(false); Operation voteOp = table.get(Op.VOTEWITNESS); assertTrue(voteOp.isEnabled()); @@ -233,7 +233,7 @@ public void testOperationRegistryWithoutOsaka() { public void testOperationRegistryWithOsaka() { VMConfig.initAllowTvmOsaka(1); try { - JumpTable table = OperationRegistry.getTable(); + JumpTable table = OperationRegistry.beginExecution(false); Operation voteOp = table.get(Op.VOTEWITNESS); assertTrue(voteOp.isEnabled()); diff --git a/framework/src/test/java/org/tron/core/vm/OperationRegistryTest.java b/framework/src/test/java/org/tron/core/vm/OperationRegistryTest.java new file mode 100644 index 0000000000..2a0695b941 --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/OperationRegistryTest.java @@ -0,0 +1,89 @@ +package org.tron.core.vm; + +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.tron.core.vm.config.VMConfig; + +public class OperationRegistryTest { + + private boolean previousHigherCpuLimit; + + @Before + public void setUp() { + previousHigherCpuLimit = VMConfig.allowHigherLimitForMaxCpuTimeOfOneTx(); + } + + @After + public void tearDown() { + OperationRegistry.endExecution(true); + VMConfig.initAllowHigherLimitForMaxCpuTimeOfOneTx(previousHigherCpuLimit ? 1 : 0); + } + + @Test + public void constantExecutionUsesPrivateReusableTable() { + JumpTable shared = OperationRegistry.getTable(false); + JumpTable constant = OperationRegistry.beginExecution(true); + + assertNotSame(shared, constant); + assertSame(constant, OperationRegistry.getTable(true)); + assertSame(shared, OperationRegistry.getTable(false)); + } + + @Test + public void nonConstantExecutionAlwaysUsesSharedTable() { + JumpTable shared = OperationRegistry.getTable(false); + + assertSame(shared, OperationRegistry.beginExecution(false)); + assertSame(shared, OperationRegistry.getTable(false)); + } + + @Test(expected = IllegalStateException.class) + public void constantTableIsUnavailableAfterExecution() { + OperationRegistry.beginExecution(true); + OperationRegistry.endExecution(true); + + OperationRegistry.getTable(true); + } + + @Test + public void consecutiveConstantExecutionsUseDifferentTables() { + JumpTable first = OperationRegistry.beginExecution(true); + OperationRegistry.endExecution(true); + JumpTable second = OperationRegistry.beginExecution(true); + + assertNotSame(first, second); + } + + @Test + public void nonConstantExecutionAfterConstantExecutionUsesSharedTable() { + JumpTable shared = OperationRegistry.getTable(false); + JumpTable constant = OperationRegistry.beginExecution(true); + + try { + assertNotSame(constant, shared); + // A stale constant-call table must not affect a non-constant execution on the same thread. + assertSame(shared, OperationRegistry.beginExecution(false)); + assertSame(shared, OperationRegistry.getTable(false)); + assertSame(constant, OperationRegistry.getTable(true)); + } finally { + OperationRegistry.endExecution(true); + } + } + + @Test + public void constantAdjustmentsDoNotMutateSharedTable() { + JumpTable shared = OperationRegistry.getTable(false); + Operation sharedMload = shared.get(Op.MLOAD); + VMConfig.initAllowHigherLimitForMaxCpuTimeOfOneTx(1); + + JumpTable constant = OperationRegistry.beginExecution(true); + + assertSame(sharedMload, shared.get(Op.MLOAD)); + assertNotSame(sharedMload, constant.get(Op.MLOAD)); + assertSame(constant.get(Op.MLOAD), OperationRegistry.getTable(true).get(Op.MLOAD)); + } +} diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/StakeV2AfterSelfDestructTest.java b/framework/src/test/java/org/tron/core/vm/nativecontract/StakeV2AfterSelfDestructTest.java new file mode 100644 index 0000000000..a3c59019b6 --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/StakeV2AfterSelfDestructTest.java @@ -0,0 +1,186 @@ +package org.tron.core.vm.nativecontract; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.tron.core.config.Parameter.ChainConstant.TRX_PRECISION; +import static org.tron.core.config.Parameter.ForkBlockVersionEnum.VERSION_4_8_2_2; +import static org.tron.protos.contract.Common.ResourceCode.BANDWIDTH; +import static org.tron.protos.contract.Common.ResourceCode.ENERGY; + +import com.google.protobuf.ByteString; +import org.junit.Assert; +import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.tron.common.utils.DecodeUtil; +import org.tron.common.utils.ForkController; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.store.DynamicPropertiesStore; +import org.tron.core.vm.nativecontract.param.CancelAllUnfreezeV2Param; +import org.tron.core.vm.nativecontract.param.FreezeBalanceV2Param; +import org.tron.core.vm.nativecontract.param.UnfreezeBalanceV2Param; +import org.tron.core.vm.nativecontract.param.WithdrawExpireUnfreezeParam; +import org.tron.core.vm.program.Program.OutOfTimeException; +import org.tron.core.vm.repository.Repository; +import org.tron.protos.Protocol; +import org.tron.protos.contract.Common.ResourceCode; + +public class StakeV2AfterSelfDestructTest { + + private static final long NOW = 1_000L; + + @Test + public void freezeAfterSelfDestructIsForkGated() throws Exception { + byte[] ownerAddress = address(1); + AccountCapsule owner = account(ownerAddress, 0, 0); + owner.setBalance(TRX_PRECISION); + + Repository repository = mock(Repository.class); + DynamicPropertiesStore dynamicStore = mock(DynamicPropertiesStore.class); + when(repository.getDynamicPropertiesStore()).thenReturn(dynamicStore); + when(repository.getAccount(ownerAddress)).thenReturn(owner); + when(repository.isSelfDestructed(ownerAddress)).thenReturn(true); + + FreezeBalanceV2Param param = new FreezeBalanceV2Param(); + param.setOwnerAddress(ownerAddress); + param.setFrozenBalance(TRX_PRECISION); + param.setResourceType(BANDWIDTH); + FreezeBalanceV2Processor processor = new FreezeBalanceV2Processor(); + + ForkController forkController = mock(ForkController.class); + try (MockedStatic fork = Mockito.mockStatic(ForkController.class)) { + fork.when(ForkController::instance).thenReturn(forkController); + when(forkController.pass(VERSION_4_8_2_2)).thenReturn(false); + processor.validate(param, repository); + + when(forkController.pass(VERSION_4_8_2_2)).thenReturn(true); + assertFreezeV2Timeout(() -> processor.validate(param, repository)); + } + } + + @Test + public void invalidDelegatedBalancesBlockWithdrawAndCancelAfterFork() throws Exception { + byte[] ownerAddress = address(1); + Repository repository = mock(Repository.class); + DynamicPropertiesStore dynamicStore = mock(DynamicPropertiesStore.class); + when(repository.getDynamicPropertiesStore()).thenReturn(dynamicStore); + when(dynamicStore.getLatestBlockHeaderTimestamp()).thenReturn(NOW); + + WithdrawExpireUnfreezeParam withdrawParam = new WithdrawExpireUnfreezeParam(); + withdrawParam.setOwnerAddress(ownerAddress); + WithdrawExpireUnfreezeProcessor withdrawProcessor = + new WithdrawExpireUnfreezeProcessor(); + CancelAllUnfreezeV2Param cancelParam = new CancelAllUnfreezeV2Param(); + cancelParam.setOwnerAddress(ownerAddress); + CancelAllUnfreezeV2Processor cancelProcessor = new CancelAllUnfreezeV2Processor(); + + ForkController forkController = mock(ForkController.class); + try (MockedStatic fork = Mockito.mockStatic(ForkController.class)) { + fork.when(ForkController::instance).thenReturn(forkController); + when(forkController.pass(VERSION_4_8_2_2)).thenReturn(false); + when(repository.getAccount(ownerAddress)).thenReturn(account(ownerAddress, -1, 0)); + withdrawProcessor.validate(withdrawParam, repository); + cancelProcessor.validate(cancelParam, repository); + when(repository.getAccount(ownerAddress)).thenReturn(account(ownerAddress, 0, -1)); + withdrawProcessor.validate(withdrawParam, repository); + cancelProcessor.validate(cancelParam, repository); + + when(forkController.pass(VERSION_4_8_2_2)).thenReturn(true); + when(repository.getAccount(ownerAddress)).thenReturn(account(ownerAddress, -1, 0)); + assertInvalidDelegatedV2Timeout( + () -> withdrawProcessor.validate(withdrawParam, repository)); + assertInvalidDelegatedV2Timeout( + () -> cancelProcessor.validate(cancelParam, repository)); + when(repository.getAccount(ownerAddress)).thenReturn(account(ownerAddress, 0, -1)); + assertInvalidDelegatedV2Timeout( + () -> withdrawProcessor.validate(withdrawParam, repository)); + assertInvalidDelegatedV2Timeout( + () -> cancelProcessor.validate(cancelParam, repository)); + } + } + + @Test + public void invalidDelegatedBalancesBlockUnfreezeAfterFork() throws Exception { + byte[] ownerAddress = address(1); + Repository repository = mock(Repository.class); + DynamicPropertiesStore dynamicStore = mock(DynamicPropertiesStore.class); + when(repository.getDynamicPropertiesStore()).thenReturn(dynamicStore); + when(dynamicStore.getLatestBlockHeaderTimestamp()).thenReturn(NOW); + + UnfreezeBalanceV2Param bandwidthParam = unfreezeParam(ownerAddress, BANDWIDTH); + UnfreezeBalanceV2Param energyParam = unfreezeParam(ownerAddress, ENERGY); + UnfreezeBalanceV2Processor processor = new UnfreezeBalanceV2Processor(); + + ForkController forkController = mock(ForkController.class); + try (MockedStatic fork = Mockito.mockStatic(ForkController.class)) { + fork.when(ForkController::instance).thenReturn(forkController); + when(forkController.pass(VERSION_4_8_2_2)).thenReturn(false); + when(repository.getAccount(ownerAddress)).thenReturn( + accountWithFrozenV2(ownerAddress, -1, 0, BANDWIDTH)); + processor.validate(bandwidthParam, repository); + when(repository.getAccount(ownerAddress)).thenReturn( + accountWithFrozenV2(ownerAddress, 0, -1, ENERGY)); + processor.validate(energyParam, repository); + + when(forkController.pass(VERSION_4_8_2_2)).thenReturn(true); + when(repository.getAccount(ownerAddress)).thenReturn( + accountWithFrozenV2(ownerAddress, -1, 0, BANDWIDTH)); + assertInvalidDelegatedV2Timeout( + () -> processor.validate(bandwidthParam, repository)); + when(repository.getAccount(ownerAddress)).thenReturn( + accountWithFrozenV2(ownerAddress, 0, -1, ENERGY)); + assertInvalidDelegatedV2Timeout(() -> processor.validate(energyParam, repository)); + } + } + + private static AccountCapsule account(byte[] address, long bandwidth, long energy) { + Protocol.Account.AccountResource resource = Protocol.Account.AccountResource.newBuilder() + .setDelegatedFrozenV2BalanceForEnergy(energy) + .build(); + Protocol.Account account = Protocol.Account.newBuilder() + .setAddress(ByteString.copyFrom(address)) + .setDelegatedFrozenV2BalanceForBandwidth(bandwidth) + .setAccountResource(resource) + .build(); + return new AccountCapsule(account); + } + + private static AccountCapsule accountWithFrozenV2( + byte[] address, long bandwidth, long energy, ResourceCode resourceCode) { + AccountCapsule accountCapsule = account(address, bandwidth, energy); + if (resourceCode == BANDWIDTH) { + accountCapsule.addFrozenBalanceForBandwidthV2(TRX_PRECISION); + } else { + accountCapsule.addFrozenBalanceForEnergyV2(TRX_PRECISION); + } + return accountCapsule; + } + + private static UnfreezeBalanceV2Param unfreezeParam( + byte[] ownerAddress, ResourceCode resourceCode) { + UnfreezeBalanceV2Param param = new UnfreezeBalanceV2Param(); + param.setOwnerAddress(ownerAddress); + param.setResourceType(resourceCode); + param.setUnfreezeBalance(TRX_PRECISION); + return param; + } + + private static byte[] address(int suffix) { + byte[] address = new byte[21]; + address[0] = DecodeUtil.addressPreFixByte; + address[address.length - 1] = (byte) suffix; + return address; + } + + private static void assertFreezeV2Timeout(ThrowingRunnable runnable) { + OutOfTimeException exception = Assert.assertThrows(OutOfTimeException.class, runnable); + Assert.assertEquals( + "CPU timeout for FreezeBalanceV2 after SELFDESTRUCT", exception.getMessage()); + } + + private static void assertInvalidDelegatedV2Timeout(ThrowingRunnable runnable) { + OutOfTimeException exception = Assert.assertThrows(OutOfTimeException.class, runnable); + Assert.assertEquals("CPU timeout for invalid delegated V2 balance", exception.getMessage()); + } +} diff --git a/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplSelfDestructTest.java b/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplSelfDestructTest.java new file mode 100644 index 0000000000..10131a0659 --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplSelfDestructTest.java @@ -0,0 +1,36 @@ +package org.tron.core.vm.repository; + +import org.junit.Assert; +import org.junit.Test; + +public class RepositoryImplSelfDestructTest { + + private static final byte[] ADDRESS = new byte[] {1}; + + @Test + public void committedSelfDestructMarkerIsVisibleToParentAndSibling() { + Repository root = RepositoryImpl.createRoot(null); + Repository child = root.newRepositoryChild(); + + child.markSelfDestruct(ADDRESS); + Assert.assertTrue(child.isSelfDestructed(ADDRESS)); + Assert.assertFalse(root.isSelfDestructed(ADDRESS)); + + child.commit(); + Assert.assertTrue(root.isSelfDestructed(ADDRESS)); + Assert.assertTrue(root.newRepositoryChild().isSelfDestructed(ADDRESS)); + } + + @Test + public void nestedMarkerDoesNotLeakWhenOuterCallIsReverted() { + Repository root = RepositoryImpl.createRoot(null); + Repository outerCall = root.newRepositoryChild(); + Repository nestedCall = outerCall.newRepositoryChild(); + + nestedCall.markSelfDestruct(ADDRESS); + nestedCall.commit(); + + Assert.assertTrue(outerCall.isSelfDestructed(ADDRESS)); + Assert.assertFalse(root.isSelfDestructed(ADDRESS)); + } +}