From a53fbf3003be76d47a76b1cb72c495bc4ddd4b27 Mon Sep 17 00:00:00 2001 From: Asuka Date: Tue, 27 Oct 2026 15:50:26 +0800 Subject: [PATCH 01/10] feat(vm): optimize vote validation and stake weights --- .../org/tron/core/utils/ProposalUtil.java | 18 +- .../org/tron/core/vm/config/ConfigLoader.java | 1 + .../org/tron/core/vm/program/Program.java | 37 +++ .../core/store/DynamicPropertiesStore.java | 18 ++ .../org/tron/core/vm/config/VMConfig.java | 9 + .../src/main/java/org/tron/core/Wallet.java | 5 + .../tron/core/consensus/ProposalService.java | 5 + .../core/vm/nativecontract/Tip945Test.java | 271 ++++++++++++++++++ 8 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java diff --git a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java index 2d5818f17af..e988f221c0e 100644 --- a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java @@ -1029,6 +1029,21 @@ public static void validator(DynamicPropertiesStore dynamicPropertiesStore, } break; } + case ALLOW_TVM_RESOURCE_OPTIMIZATION: { + if (!forkController.pass(ForkBlockVersionEnum.VERSION_4_8_3)) { + throw new ContractValidateException( + "Bad chain parameter id [ALLOW_TVM_RESOURCE_OPTIMIZATION]"); + } + if (dynamicPropertiesStore.getAllowTvmResourceOptimization() == 1) { + throw new ContractValidateException( + "[ALLOW_TVM_RESOURCE_OPTIMIZATION] has been valid, no need to propose again"); + } + if (value != 1) { + throw new ContractValidateException( + "This value[ALLOW_TVM_RESOURCE_OPTIMIZATION] is only allowed to be 1"); + } + break; + } default: break; } @@ -1122,7 +1137,8 @@ public enum ProposalType { // current value, value range ALLOW_STRICT_ECDSA_VALIDATION(100), // 0, 1 CLOSE_EXCHANGE(101), // 0, 1, 2 ALLOW_OPTIMIZE_TVM_STORAGE_KEY(102), // 0, 1 - ALLOW_TVM_VOTE_POWER_OPTIMIZATION(103); // 0, 1 + ALLOW_TVM_VOTE_POWER_OPTIMIZATION(103), // 0, 1 + ALLOW_TVM_RESOURCE_OPTIMIZATION(104); // 0, 1 private long code; ProposalType(long code) { diff --git a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java index c364ede87d1..66fbf9191d6 100644 --- a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java +++ b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java @@ -53,6 +53,7 @@ public static void load(StoreFactory storeFactory, boolean isolate) { snapshot.allowStrictEcdsaValidation = ds.getAllowStrictEcdsaValidation() == 1; snapshot.allowOptimizeTvmStorageKey = ds.allowOptimizeTvmStorageKey(); snapshot.allowTvmVotePowerOptimization = ds.getAllowTvmVotePowerOptimization() == 1; + snapshot.allowTvmResourceOptimization = ds.getAllowTvmResourceOptimization() == 1; if (isolate) { VMConfig.setLocalSnapshot(snapshot); } else { 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 4ed70964243..02f8b9b165b 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 @@ -11,6 +11,7 @@ import static org.tron.common.math.Maths.min; import static org.tron.common.math.Maths.multiplyExact; import static org.tron.common.utils.ByteUtil.stripLeadingZeroes; +import static org.tron.core.config.Parameter.ChainConstant.MAX_VOTE_NUMBER; import static org.tron.core.config.Parameter.ChainConstant.TRX_PRECISION; import static org.tron.protos.contract.Common.ResourceCode.BANDWIDTH; import static org.tron.protos.contract.Common.ResourceCode.ENERGY; @@ -637,6 +638,10 @@ private long transferFrozenV2BalanceToInheritor(byte[] ownerAddr, byte[] inherit AccountCapsule inheritorCapsule = repo.getAccount(inheritorAddr); long now = repo.getHeadSlot(); + long[] weightsBefore = VMConfig.allowTvmResourceOptimization() + && ownerCapsule.getFrozenV2List().stream().anyMatch(frozen -> frozen.getAmount() > 0) + ? frozenV2Weights(ownerCapsule, inheritorCapsule) : null; + if (optimizeVotePowerState(repo) && ownerCapsule.getFrozenV2List().stream().anyMatch(frozen -> frozen.getAmount() > 0)) { if (inheritorCapsule.oldTronPowerIsNotInitialized()) { @@ -701,9 +706,34 @@ private long transferFrozenV2BalanceToInheritor(byte[] ownerAddr, byte[] inherit clearOwnerFreezeV2(ownerCapsule); repo.updateAccount(ownerCapsule.createDbKey(), ownerCapsule); repo.updateAccount(inheritorCapsule.createDbKey(), inheritorCapsule); + if (weightsBefore != null) { + long[] weightsAfter = frozenV2Weights(repo.getAccount(ownerAddr), repo.getAccount(inheritorAddr)); + repo.addTotalNetWeight(weightsAfter[0] - weightsBefore[0]); + repo.addTotalEnergyWeight(weightsAfter[1] - weightsBefore[1]); + repo.addTotalTronPowerWeight(weightsAfter[2] - weightsBefore[2]); + } return expireUnfrozenBalance; } + private long[] frozenV2Weights(AccountCapsule source, AccountCapsule beneficiary) { + long[] weights = accountFrozenV2Weights(source); + if (!FastByteComparisons.isEqual(source.createDbKey(), beneficiary.createDbKey())) { + long[] beneficiaryWeights = accountFrozenV2Weights(beneficiary); + for (int i = 0; i < weights.length; i++) { + weights[i] = addExact(weights[i], beneficiaryWeights[i], VMConfig.disableJavaLangMath()); + } + } + return weights; + } + + private long[] accountFrozenV2Weights(AccountCapsule account) { + return new long[] { + account.getFrozenV2BalanceWithDelegated(BANDWIDTH) / TRX_PRECISION, + account.getFrozenV2BalanceWithDelegated(ENERGY) / TRX_PRECISION, + account.getTronPowerFrozenV2Balance() / TRX_PRECISION + }; + } + private void clearOwnerFreeze(AccountCapsule ownerCapsule) { ownerCapsule.setFrozenForBandwidth(0, 0); ownerCapsule.setFrozenForEnergy(0, 0); @@ -2324,6 +2354,13 @@ public boolean voteWitness(int witnessArrayOffset, int witnessArrayLength, return false; } + if (VMConfig.allowTvmResourceOptimization() && witnessArrayLength > MAX_VOTE_NUMBER) { + if (internalTx != null) { + internalTx.reject(); + } + return false; + } + try { VoteWitnessParam param = new VoteWitnessParam(); param.setVoterAddress(owner); diff --git a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java index 920a43929aa..b4438fc2540 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -270,6 +270,9 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking private static final byte[] ALLOW_TVM_VOTE_POWER_OPTIMIZATION = "ALLOW_TVM_VOTE_POWER_OPTIMIZATION".getBytes(); + private static final byte[] ALLOW_TVM_RESOURCE_OPTIMIZATION = + "ALLOW_TVM_RESOURCE_OPTIMIZATION".getBytes(); + @Autowired private DynamicPropertiesStore(@Value("properties") String dbName) { super(dbName); @@ -3167,6 +3170,21 @@ public boolean allowTvmVotePowerOptimization() { return getAllowTvmVotePowerOptimization() == 1L; } + public long getAllowTvmResourceOptimization() { + return Optional.ofNullable(getUnchecked(ALLOW_TVM_RESOURCE_OPTIMIZATION)) + .map(BytesCapsule::getData) + .map(ByteArray::toLong) + .orElse(0L); + } + + public void saveAllowTvmResourceOptimization(long value) { + this.put(ALLOW_TVM_RESOURCE_OPTIMIZATION, new BytesCapsule(ByteArray.fromLong(value))); + } + + public boolean allowTvmResourceOptimization() { + return getAllowTvmResourceOptimization() == 1L; + } + public boolean allowTvmStorageOptimization() { return allowOptimizeTvmStorage() || allowOptimizeTvmStorageKey(); } diff --git a/common/src/main/java/org/tron/core/vm/config/VMConfig.java b/common/src/main/java/org/tron/core/vm/config/VMConfig.java index 0a9b5f8b2cd..2b2ea08588a 100644 --- a/common/src/main/java/org/tron/core/vm/config/VMConfig.java +++ b/common/src/main/java/org/tron/core/vm/config/VMConfig.java @@ -47,6 +47,7 @@ public static class Snapshot { public boolean allowStrictEcdsaValidation; public boolean allowOptimizeTvmStorageKey; public boolean allowTvmVotePowerOptimization; + public boolean allowTvmResourceOptimization; } // HEAD / block-processing config, written by the consensus path; read by everyone with no @@ -209,6 +210,10 @@ public static void initAllowTvmVotePowerOptimization(long allow) { globalSnapshot.allowTvmVotePowerOptimization = allow == 1; } + public static void initAllowTvmResourceOptimization(long allow) { + globalSnapshot.allowTvmResourceOptimization = allow == 1; + } + public static void initAllowStrictEcdsaValidation(long allow) { globalSnapshot.allowStrictEcdsaValidation = allow == 1; } @@ -329,6 +334,10 @@ public static boolean allowTvmVotePowerOptimization() { return current().allowTvmVotePowerOptimization; } + public static boolean allowTvmResourceOptimization() { + return current().allowTvmResourceOptimization; + } + public static boolean allowStrictEcdsaValidation() { return current().allowStrictEcdsaValidation; } diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 0434cffd194..eea50879d9a 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -1703,6 +1703,11 @@ public Protocol.ChainParameters getChainParameters() { .setValue(dbManager.getDynamicPropertiesStore().getAllowTvmVotePowerOptimization()) .build()); + builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() + .setKey("getAllowTvmResourceOptimization") + .setValue(dbManager.getDynamicPropertiesStore().getAllowTvmResourceOptimization()) + .build()); + return builder.build(); } diff --git a/framework/src/main/java/org/tron/core/consensus/ProposalService.java b/framework/src/main/java/org/tron/core/consensus/ProposalService.java index c8643ec0e6d..2fad719dedf 100644 --- a/framework/src/main/java/org/tron/core/consensus/ProposalService.java +++ b/framework/src/main/java/org/tron/core/consensus/ProposalService.java @@ -435,6 +435,11 @@ public static boolean process(Manager manager, ProposalCapsule proposalCapsule) .saveAllowTvmVotePowerOptimization(entry.getValue()); break; } + case ALLOW_TVM_RESOURCE_OPTIMIZATION: { + manager.getDynamicPropertiesStore() + .saveAllowTvmResourceOptimization(entry.getValue()); + break; + } default: find = false; break; diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java new file mode 100644 index 00000000000..3f1ee0b0d0e --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java @@ -0,0 +1,271 @@ +package org.tron.core.vm.nativecontract; + +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.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.tron.core.config.Parameter.ChainConstant.TRX_PRECISION; +import static org.tron.protos.contract.Common.ResourceCode.BANDWIDTH; +import static org.tron.protos.contract.Common.ResourceCode.ENERGY; +import static org.tron.protos.contract.Common.ResourceCode.TRON_POWER; + +import com.google.protobuf.ByteString; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.common.runtime.InternalTransaction; +import org.tron.common.runtime.vm.DataWord; +import org.tron.common.utils.ForkController; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.capsule.ProposalCapsule; +import org.tron.core.capsule.WitnessCapsule; +import org.tron.core.config.Parameter.ForkBlockVersionEnum; +import org.tron.core.config.args.Args; +import org.tron.core.consensus.ProposalService; +import org.tron.core.exception.ContractValidateException; +import org.tron.core.store.StoreFactory; +import org.tron.core.utils.ProposalUtil; +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.invoke.ProgramInvoke; +import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.repository.RepositoryImpl; +import org.tron.protos.Protocol; +import org.tron.protos.contract.Common.ResourceCode; + +public class Tip945Test extends BaseTest { + private static final byte[] OWNER = address(11); + private static final byte[] BENEFICIARY = address(12); + private static final byte[] WITNESS = address(13); + private Repository repo; + + @BeforeClass + public static void init() { + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); + } + + @Before + public void setup() { + VMConfig.setGlobalSnapshot(new VMConfig.Snapshot()); + ConfigLoader.disable = true; + VMConfig.initAllowTvmFreezeV2(1); + VMConfig.initAllowTvmSelfdestructRestriction(1); + VMConfig.initAllowTvmResourceOptimization(1); + dbManager.getDynamicPropertiesStore().saveAllowNewResourceModel(1); + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowTvmResourceOptimization(0); + repo = RepositoryImpl.createRoot(StoreFactory.getInstance()); + repo.createAccount(OWNER, Protocol.AccountType.Contract); + repo.createAccount(BENEFICIARY, Protocol.AccountType.Contract); + dbManager.getWitnessStore().put(WITNESS, new WitnessCapsule( + Protocol.Witness.newBuilder().setAddress(ByteString.copyFrom(WITNESS)).build())); + } + + private static byte[] address(int suffix) { + byte[] bytes = new byte[21]; + bytes[0] = 0x41; + bytes[20] = (byte) suffix; + return bytes; + } + + private Program program(Repository repository, byte[] owner) { + ProgramInvoke invoke = mock(ProgramInvoke.class); + when(invoke.getDeposit()).thenReturn(repository); + when(invoke.getContractAddress()).thenReturn(new DataWord(owner)); + when(invoke.getTimestamp()).thenReturn(new DataWord(1)); + InternalTransaction transaction = mock(InternalTransaction.class); + when(transaction.getHash()).thenReturn(new byte[32]); + return new Program(new byte[0], owner, invoke, transaction); + } + + private void stake(byte[] owner, ResourceCode type, long amount) { + AccountCapsule account = repo.getAccount(owner); + switch (type) { + case BANDWIDTH: + account.addFrozenBalanceForBandwidthV2(amount); + break; + case ENERGY: + account.addFrozenBalanceForEnergyV2(amount); + break; + case TRON_POWER: + account.addFrozenForTronPowerV2(amount); + break; + default: + throw new IllegalArgumentException("Unsupported stake type"); + } + repo.updateAccount(owner, account); + } + + private void legacy(byte[] owner, long value) { + AccountCapsule account = repo.getAccount(owner); + account.setOldTronPower(value); + repo.updateAccount(owner, account); + } + + private long legacy(byte[] owner) { + return repo.getAccount(owner).getInstance().getOldTronPower(); + } + + @Test + public void governanceRequiresForkAndOneWayActivation() throws Exception { + ForkController fork = mock(ForkController.class); + assertEquals(0, dbManager.getDynamicPropertiesStore().getAllowTvmResourceOptimization()); + assertThrows(ContractValidateException.class, () -> ProposalUtil.validator( + dbManager.getDynamicPropertiesStore(), fork, 104, 1)); + when(fork.pass(ForkBlockVersionEnum.VERSION_4_8_3)).thenReturn(true); + for (long value : new long[]{-1, 0, 2}) { + assertThrows(ContractValidateException.class, () -> ProposalUtil.validator( + dbManager.getDynamicPropertiesStore(), fork, 104, value)); + } + ProposalUtil.validator(dbManager.getDynamicPropertiesStore(), fork, 104, 1); + assertTrue(ProposalService.process(dbManager, new ProposalCapsule( + Protocol.Proposal.newBuilder().putParameters(104, 1).build()))); + assertEquals(1, dbManager.getDynamicPropertiesStore().getAllowTvmResourceOptimization()); + assertThrows(ContractValidateException.class, () -> ProposalUtil.validator( + dbManager.getDynamicPropertiesStore(), fork, 104, 1)); + ConfigLoader.disable = false; + ConfigLoader.load(StoreFactory.getInstance(), false); + assertTrue(VMConfig.allowTvmResourceOptimization()); + dbManager.getDynamicPropertiesStore().saveAllowTvmResourceOptimization(0); + ConfigLoader.load(StoreFactory.getInstance(), true); + assertFalse(VMConfig.allowTvmResourceOptimization()); + VMConfig.clearLocalSnapshot(); + assertTrue(VMConfig.allowTvmResourceOptimization()); + } + + private Program arrays(int witnessLength, int amountLength) { + Program p = spy(program(repo, OWNER)); + p.memorySave(0, new DataWord(witnessLength).getData()); + p.memorySave(2048, new DataWord(amountLength).getData()); + return p; + } + + @Test + public void oversizeRejectsBeforeAnyPayloadWorkWithExpectedTrace() { + Program p = arrays(31, 31); + assertFalse(p.voteWitness(0, 31, 2048, 31)); + verify(p, never()).memoryChunk(anyInt(), anyInt()); + assertEquals(1, p.getNonce()); + assertEquals(1, p.getResult().getInternalTransactions().size()); + InternalTransaction tx = p.getResult().getInternalTransactions().get(0); + assertTrue(tx.isRejected()); + assertEquals("", tx.getExtra()); + assertNull(repo.getVotes(OWNER)); + } + + @Test + public void metadataAndLengthChecksRetainPriority() { + Program p = arrays(32, 31); + assertThrows(Program.BytecodeExecutionException.class, + () -> p.voteWitness(0, 31, 2048, 31)); + Program mismatch = arrays(31, 30); + assertFalse(mismatch.voteWitness(0, 31, 2048, 30)); + assertFalse(mismatch.getResult().getInternalTransactions().get(0).isRejected()); + verify(mismatch, never()).memoryChunk(anyInt(), anyInt()); + } + + @Test + public void zeroAndMaximumAreAcceptedAndLegacyLimitPathStillBuildsPayload() { + Program empty = arrays(0, 0); + assertTrue(empty.voteWitness(0, 0, 2048, 0)); + Program maximum = arrays(30, 30); + for (int i = 0; i < 30; i++) { + maximum.memorySave(32 + 32 * i, new DataWord(WITNESS).getData()); + } + assertTrue(maximum.voteWitness(0, 30, 2048, 30)); + VMConfig.initAllowTvmResourceOptimization(0); + Program legacy = arrays(31, 31); + assertFalse(legacy.voteWitness(0, 31, 2048, 31)); + verify(legacy, times(2)).memoryChunk(anyInt(), anyInt()); + assertFalse(legacy.getResult().getInternalTransactions().get(0).getExtra().isEmpty()); + } + + private long[] weights() { + return new long[]{repo.getTotalNetWeight(), repo.getTotalEnergyWeight(), + repo.getTotalTronPowerWeight()}; + } + + @Test + public void independentlyRoundsEachStakeTypeAndIncludesBeneficiaryDelegation() { + for (ResourceCode type : new ResourceCode[]{BANDWIDTH, ENERGY, TRON_POWER}) { + for (long beneficiaryAmount : new long[]{2_300_000, 2_400_000, 2_600_000}) { + setup(); + stake(OWNER, type, 1_600_000); + stake(BENEFICIARY, type, beneficiaryAmount); + long[] before = weights(); + program(repo, OWNER).suicide2(new DataWord(BENEFICIARY)); + long[] after = weights(); + for (int i = 0; i < 3; i++) { + assertEquals(i == type.getNumber() && beneficiaryAmount >= 2_400_000 ? 1 : 0, + after[i] - before[i]); + } + } + } + setup(); + stake(OWNER, BANDWIDTH, 1_600_000); + stake(BENEFICIARY, BANDWIDTH, 600_000); + AccountCapsule beneficiary = repo.getAccount(BENEFICIARY); + beneficiary.addDelegatedFrozenV2BalanceForBandwidth(600_000); + repo.updateAccount(BENEFICIARY, beneficiary); + long before = repo.getTotalNetWeight(); + program(repo, OWNER).suicide2(new DataWord(BENEFICIARY)); + assertEquals(before, repo.getTotalNetWeight()); + } + + @Test + public void sequentialTransfersAndDeletedSourcesUseOperationLocalSnapshots() { + stake(OWNER, ENERGY, 1_600_000); + stake(BENEFICIARY, ENERGY, 2_600_000); + long before = repo.getTotalEnergyWeight(); + Program p = program(repo, OWNER); + p.suicide(new DataWord(BENEFICIARY)); + assertEquals(1, p.getResult().getDeleteAccounts().size()); + assertEquals(before + 1, repo.getTotalEnergyWeight()); + program(repo, OWNER).suicide(new DataWord(BENEFICIARY)); + assertEquals(before + 1, repo.getTotalEnergyWeight()); + } + + @Test + public void selfBeneficiaryAndRevertedTransferLeaveParentWeightsUnchanged() { + stake(OWNER, ENERGY, 1_600_000); + stake(BENEFICIARY, ENERGY, 2_600_000); + long[] before = weights(); + program(repo, OWNER).suicide2(new DataWord(OWNER)); + assertArrayEquals(before, weights()); + Repository child = repo.newRepositoryChild(); + program(child, OWNER).suicide2(new DataWord(BENEFICIARY)); + assertEquals(before[1] + 1, child.getTotalEnergyWeight()); + assertArrayEquals(before, weights()); + assertEquals(1_600_000, repo.getAccount(OWNER).getFrozenV2BalanceForEnergy()); + } + + @Test + public void disabledFlagAndNonTransferredBalancesLeaveWeightsUnchanged() { + stake(OWNER, BANDWIDTH, 1_600_000); + stake(BENEFICIARY, BANDWIDTH, 2_600_000); + VMConfig.initAllowTvmResourceOptimization(0); + long[] before = weights(); + program(repo, OWNER).suicide2(new DataWord(BENEFICIARY)); + assertArrayEquals(before, weights()); + setup(); + AccountCapsule owner = repo.getAccount(OWNER); + owner.setBalance(3 * TRX_PRECISION); + owner.addUnfrozenV2List(ENERGY, 2 * TRX_PRECISION, 0); + repo.updateAccount(OWNER, owner); + before = weights(); + program(repo, OWNER).suicide2(new DataWord(BENEFICIARY)); + assertArrayEquals(before, weights()); + } +} From 008bb696c0852157f8773b2e9f2b2e9befb44f10 Mon Sep 17 00:00:00 2001 From: Asuka Date: Tue, 27 Oct 2026 16:51:18 +0800 Subject: [PATCH 02/10] test(vm): cover opcode guards and consecutive stake transfers --- .../core/vm/nativecontract/Tip945Test.java | 81 +++++++++++++ .../core/vm/nativecontract/Tip945VmTest.java | 110 ++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 framework/src/test/java/org/tron/core/vm/nativecontract/Tip945VmTest.java diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java index 3f1ee0b0d0e..45bb9b55f25 100644 --- a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java @@ -36,6 +36,9 @@ import org.tron.core.exception.ContractValidateException; import org.tron.core.store.StoreFactory; import org.tron.core.utils.ProposalUtil; +import org.tron.core.vm.Op; +import org.tron.core.vm.OperationRegistry; +import org.tron.core.vm.VM; import org.tron.core.vm.config.ConfigLoader; import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.program.Program; @@ -268,4 +271,82 @@ public void disabledFlagAndNonTransferredBalancesLeaveWeightsUnchanged() { program(repo, OWNER).suicide2(new DataWord(BENEFICIARY)); assertArrayEquals(before, weights()); } + + private Program opcodeProgram(boolean staticCall, long energy) { + VMConfig.initAllowTvmVote(1); + ProgramInvoke invoke = mock(ProgramInvoke.class); + when(invoke.getDeposit()).thenReturn(repo); + when(invoke.getContractAddress()).thenReturn(new DataWord(OWNER)); + when(invoke.isStaticCall()).thenReturn(staticCall); + when(invoke.getEnergyLimit()).thenReturn(energy); + when(invoke.getVmShouldEndInUs()).thenReturn(Long.MAX_VALUE); + InternalTransaction transaction = mock(InternalTransaction.class); + when(transaction.getHash()).thenReturn(new byte[32]); + Program p = new Program(new byte[]{(byte) Op.VOTEWITNESS}, OWNER, invoke, transaction); + p.setRootTransactionId(new byte[32]); + p.memorySave(0, new DataWord(31).getData()); + p.memorySave(2048, new DataWord(31).getData()); + p.stackPush(new DataWord(0)); + p.stackPush(new DataWord(31)); + p.stackPush(new DataWord(2048)); + p.stackPush(new DataWord(31)); + return p; + } + + @Test + public void actualVoteOpcodePushesZeroForOversizedInput() { + Program p = opcodeProgram(false, 1_000_000); + VM.play(p, OperationRegistry.prepareAndGetTable(false)); + assertNull(p.getResult().getException()); + assertEquals(1, p.getStack().size()); + assertEquals(DataWord.ZERO(), p.getStack().peek()); + assertEquals(1, p.getNonce()); + assertTrue(p.getResult().getInternalTransactions().get(0).isRejected()); + assertEquals("", p.getResult().getInternalTransactions().get(0).getExtra()); + assertNull(repo.getVotes(OWNER)); + } + + @Test + public void actualOpcodeEnergyAndStaticChecksPrecedeCountRejection() { + for (long activation : new long[]{0, 1}) { + VMConfig.initAllowTvmResourceOptimization(activation); + Program insufficient = opcodeProgram(true, 1); + VM.play(insufficient, OperationRegistry.prepareAndGetTable(false)); + assertTrue(insufficient.getResult().getException() instanceof Program.OutOfEnergyException); + assertEquals(0, insufficient.getNonce()); + assertTrue(insufficient.getResult().getInternalTransactions().isEmpty()); + Program staticCall = opcodeProgram(true, 1_000_000); + VM.play(staticCall, OperationRegistry.prepareAndGetTable(false)); + assertTrue(staticCall.getResult().getException() + instanceof Program.StaticCallModificationException); + assertEquals(0, staticCall.getNonce()); + assertTrue(staticCall.getResult().getInternalTransactions().isEmpty()); + assertNull(repo.getVotes(OWNER)); + } + } + + @Test + public void independentActivationPreservesVotingStateAndWeightBoundaries() { + for (long power : new long[]{0, 1}) { + for (long resource : new long[]{0, 1}) { + setup(); + VMConfig.initAllowTvmVotePowerOptimization(power); + VMConfig.initAllowTvmResourceOptimization(resource); + stake(OWNER, BANDWIDTH, 1_600_000); + stake(BENEFICIARY, BANDWIDTH, 2_600_000); + Program p = program(repo, OWNER); + p.memorySave(0, new DataWord(31).getData()); + p.memorySave(2048, new DataWord(31).getData()); + assertFalse(p.voteWitness(0, 31, 2048, 31)); + assertEquals(0, legacy(OWNER)); + assertEquals(resource == 1, + p.getResult().getInternalTransactions().get(0).getExtra().isEmpty()); + long before = repo.getTotalNetWeight(); + p.suicide2(new DataWord(BENEFICIARY)); + assertEquals(before + resource, repo.getTotalNetWeight()); + assertEquals(power == 1 ? -1 : 0, legacy(OWNER)); + assertEquals(power == 1 ? 2_600_000 : 0, legacy(BENEFICIARY)); + } + } + } } diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945VmTest.java b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945VmTest.java new file mode 100644 index 00000000000..51abfda88f2 --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945VmTest.java @@ -0,0 +1,110 @@ +package org.tron.core.vm.nativecontract; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.tron.core.config.Parameter.ChainConstant.TRX_PRECISION; +import static org.tron.protos.contract.Common.ResourceCode.ENERGY; + +import org.junit.Test; +import org.tron.core.db.TransactionTrace; +import org.tron.core.vm.config.VMConfig; + +public class Tip945VmTest extends TvmOptimizationVmTestBase { + @Override + protected void enableOptimization() { + VMConfig.initAllowTvmResourceOptimization(1); + dbManager.getDynamicPropertiesStore().saveAllowTvmResourceOptimization(1); + } + + @Test + public void outerRevertRestoresSuccessfulChildTransferAndWeightDelta() throws Exception { + VMConfig.initAllowTvmSelfdestructRestriction(0); + install(SOURCE, destroy(BENEFICIARY)); + install(OUTER, outer(SOURCE, true)); + stake(SOURCE, ENERGY, 1_600_000); + stake(BENEFICIARY, ENERGY, 2_600_000); + legacyVotes(SOURCE, 3 * TRX_PRECISION); + byte[] sourceBefore = account(SOURCE).getData(); + byte[] beneficiaryBefore = account(BENEFICIARY).getData(); + long[] beforeWeights = weights(); + TransactionTrace trace = execute(OUTER); + assertTrue(trace.getRuntime().getResult().isRevert()); + assertEquals(1, returnWord(trace, 1)); + assertRejectedInternal(trace, "suicide"); + assertTrue(trace.getRuntime().getResult().getDeleteAccounts().isEmpty()); + finish(trace); + assertArrayEquals(sourceBefore, account(SOURCE).getData()); + assertArrayEquals(beneficiaryBefore, account(BENEFICIARY).getData()); + assertArrayEquals(beforeWeights, weights()); + assertNull(dbManager.getVotesStore().get(SOURCE)); + } + + @Test + public void physicalDeletionDoesNotApplyWeightDeltaTwice() throws Exception { + VMConfig.initAllowTvmSelfdestructRestriction(0); + install(SOURCE, destroy(BENEFICIARY)); + stake(SOURCE, ENERGY, 1_600_000); + stake(BENEFICIARY, ENERGY, 2_600_000); + legacyVotes(SOURCE, 3 * TRX_PRECISION); + long before = weights()[1]; + TransactionTrace trace = execute(SOURCE); + assertSuccess(trace); + assertNotNull(account(SOURCE)); + assertEquals(0, account(SOURCE).getFrozenV2BalanceForEnergy()); + assertEquals(1, trace.getRuntime().getResult().getDeleteAccounts().size()); + assertEquals(before + 1, weights()[1]); + finish(trace); + assertNull(account(SOURCE)); + assertEquals(before + 1, weights()[1]); + assertEquals(4_200_000, account(BENEFICIARY).getFrozenV2BalanceForEnergy()); + } + + @Test + public void twoPositiveTransfersEachAddOneWeightUsingUpdatedBalances() throws Exception { + install(SOURCE, destroy(BENEFICIARY)); + install(BENEFICIARY, destroy(THIRD)); + install(THIRD, "00"); + stake(SOURCE, ENERGY, 1_600_000); + stake(BENEFICIARY, ENERGY, 2_600_000); + stake(THIRD, ENERGY, 1_900_000); + long before = weights()[1]; + TransactionTrace first = execute(SOURCE); + assertSuccess(first); + assertEquals(before + 1, weights()[1]); + assertEquals(0, account(SOURCE).getFrozenV2BalanceForEnergy()); + assertEquals(4_200_000, account(BENEFICIARY).getFrozenV2BalanceForEnergy()); + finish(first); + TransactionTrace second = execute(BENEFICIARY); + assertSuccess(second); + assertEquals(before + 2, weights()[1]); + assertEquals(0, account(BENEFICIARY).getFrozenV2BalanceForEnergy()); + assertEquals(6_100_000, account(THIRD).getFrozenV2BalanceForEnergy()); + finish(second); + assertEquals(before + 2, weights()[1]); + } + + @Test + public void twoPositiveTransfersWithinOneTransactionUseChildCommitState() throws Exception { + install(SOURCE, destroy(BENEFICIARY)); + install(BENEFICIARY, destroy(THIRD)); + install(THIRD, "00"); + install(OUTER, twoCalls(SOURCE, BENEFICIARY, 0)); + stake(SOURCE, ENERGY, 1_600_000); + stake(BENEFICIARY, ENERGY, 2_600_000); + stake(THIRD, ENERGY, 1_900_000); + long before = weights()[1]; + TransactionTrace trace = execute(OUTER); + assertSuccess(trace); + assertEquals(1, returnWord(trace, 1)); + assertEquals(1, returnWord(trace, 2)); + assertEquals(before + 2, weights()[1]); + assertEquals(0, account(SOURCE).getFrozenV2BalanceForEnergy()); + assertEquals(0, account(BENEFICIARY).getFrozenV2BalanceForEnergy()); + assertEquals(6_100_000, account(THIRD).getFrozenV2BalanceForEnergy()); + finish(trace); + assertEquals(before + 2, weights()[1]); + } +} From 206e0f13c0c74c9fd8a3847643e682dcdc0301b5 Mon Sep 17 00:00:00 2001 From: Asuka Date: Wed, 28 Oct 2026 17:51:25 +0800 Subject: [PATCH 03/10] fix(vm): reject internal votes with mismatched array lengths --- .../java/org/tron/core/vm/program/Program.java | 3 +++ .../tron/core/vm/nativecontract/Tip945Test.java | 14 ++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) 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 02f8b9b165b..15cd7403cb7 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 @@ -2351,6 +2351,9 @@ public boolean voteWitness(int witnessArrayOffset, int witnessArrayLength, if (witnessArrayLength != amountArrayLength) { logger.warn("TVM VoteWitness: witness array length {} does not match amount array length {}", witnessArrayLength, amountArrayLength); + if (VMConfig.allowTvmResourceOptimization() && internalTx != null) { + internalTx.reject(); + } return false; } diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java index 45bb9b55f25..e2761917266 100644 --- a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java @@ -173,10 +173,16 @@ public void metadataAndLengthChecksRetainPriority() { Program p = arrays(32, 31); assertThrows(Program.BytecodeExecutionException.class, () -> p.voteWitness(0, 31, 2048, 31)); - Program mismatch = arrays(31, 30); - assertFalse(mismatch.voteWitness(0, 31, 2048, 30)); - assertFalse(mismatch.getResult().getInternalTransactions().get(0).isRejected()); - verify(mismatch, never()).memoryChunk(anyInt(), anyInt()); + for (long activation : new long[]{0, 1}) { + VMConfig.initAllowTvmResourceOptimization(activation); + Program mismatch = arrays(31, 30); + assertFalse(mismatch.voteWitness(0, 31, 2048, 30)); + InternalTransaction tx = mismatch.getResult().getInternalTransactions().get(0); + assertEquals(activation == 1, tx.isRejected()); + assertEquals("", tx.getExtra()); + assertNull(repo.getVotes(OWNER)); + verify(mismatch, never()).memoryChunk(anyInt(), anyInt()); + } } @Test From 8e3da43921263810fbbd4650466ca9b7564a27c1 Mon Sep 17 00:00:00 2001 From: Asuka Date: Thu, 29 Oct 2026 18:03:12 +0800 Subject: [PATCH 04/10] test(vm): preserve pre-existing global stake weight offsets --- .../tron/core/vm/nativecontract/Tip945Test.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java index e2761917266..c66918e7977 100644 --- a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java @@ -278,6 +278,20 @@ public void disabledFlagAndNonTransferredBalancesLeaveWeightsUnchanged() { assertArrayEquals(before, weights()); } + @Test + public void correctedTransferDeltaPreservesPreExistingGlobalWeightOffset() { + stake(OWNER, ENERGY, 1_600_000); + stake(BENEFICIARY, ENERGY, 2_600_000); + // Per-account weights total 3; retain a pre-existing global shortfall of 1. + repo.addTotalEnergyWeight(2 - repo.getTotalEnergyWeight()); + program(repo, OWNER).suicide2(new DataWord(BENEFICIARY)); + assertEquals(0, repo.getAccount(OWNER).getFrozenV2BalanceForEnergy()); + assertEquals(4_200_000, repo.getAccount(BENEFICIARY).getFrozenV2BalanceForEnergy()); + assertEquals(3, repo.getTotalEnergyWeight()); + assertEquals(1, repo.getAccount(BENEFICIARY).getFrozenV2BalanceForEnergy() + / TRX_PRECISION - repo.getTotalEnergyWeight()); + } + private Program opcodeProgram(boolean staticCall, long energy) { VMConfig.initAllowTvmVote(1); ProgramInvoke invoke = mock(ProgramInvoke.class); From 1b6053c9bbbe40178842ead2dd5ab5a8ca553da1 Mon Sep 17 00:00:00 2001 From: Asuka Date: Mon, 2 Nov 2026 16:00:41 +0800 Subject: [PATCH 05/10] fix(vm): persist restored energy window when usage is zero payEnergyBill returns before writing the account, so the Stake 2.0 reservation stays after a successful zero-energy transaction. --- .../src/main/java/org/tron/core/db/TransactionTrace.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/chainbase/src/main/java/org/tron/core/db/TransactionTrace.java b/chainbase/src/main/java/org/tron/core/db/TransactionTrace.java index 09cfc82570d..8491f26f1f7 100644 --- a/chainbase/src/main/java/org/tron/core/db/TransactionTrace.java +++ b/chainbase/src/main/java/org/tron/core/db/TransactionTrace.java @@ -276,6 +276,15 @@ && getRuntimeResult().getException() == null && !getRuntimeResult().isRevert()) receipt.getCallerEnergyMergedUsage(), receipt.getCallerEnergyMergedWindowSize(), receipt.getCallerEnergyWindowSizeV2()); + // payEnergyBill returns before storing the account when usage is zero, which keeps the + // committed Stake 2.0 reservation. Same-address origin is a second unmodified load. + if (dynamicPropertiesStore.allowTvmResourceOptimization() + && receipt.getEnergyUsageTotal() <= 0) { + if (origin != null && !caller.getAddress().equals(origin.getAddress())) { + accountStore.put(origin.getAddress().toByteArray(), origin); + } + accountStore.put(caller.getAddress().toByteArray(), caller); + } } receipt.payEnergyBill( dynamicPropertiesStore, accountStore, forkController, From 5775747eb59721c1514c672f989db7a1a60ece55 Mon Sep 17 00:00:00 2001 From: Asuka Date: Mon, 2 Nov 2026 16:00:42 +0800 Subject: [PATCH 06/10] test(vm): cover zero-energy frozen energy reservation --- .../core/vm/nativecontract/Tip945VmTest.java | 75 +++++++++++++++++++ .../TvmOptimizationVmTestBase.java | 10 ++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945VmTest.java b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945VmTest.java index 51abfda88f2..ddf31777183 100644 --- a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945VmTest.java +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945VmTest.java @@ -107,4 +107,79 @@ public void twoPositiveTransfersWithinOneTransactionUseChildCommitState() throws finish(trace); assertEquals(before + 2, weights()[1]); } + + @Test + public void zeroEnergySuccessRestoresFrozenEnergyWindows() throws Exception { + prepareEnergyWindow(); + stake(CALLER, ENERGY, 1_000L * TRX_PRECISION); + ownEnergyWeight(CALLER); + long callerUsage = account(CALLER).getEnergyUsage(); + long callerWindow = account(CALLER).getWindowSize(ENERGY); + // Caller is also the origin, so only the caller object may be stored. + TransactionTrace sameAccount = execute(BENEFICIARY); + assertZeroEnergy(sameAccount); + assertTrue(account(CALLER).getEnergyUsage() > callerUsage); + finish(sameAccount); + assertEquals(callerUsage, account(CALLER).getEnergyUsage()); + assertEquals(callerWindow, account(CALLER).getWindowSize(ENERGY)); + + install(SOURCE, "00"); + stake(SOURCE, ENERGY, 1_000L * TRX_PRECISION); + install(OUTER, "00", SOURCE, 0L, 10_000_000L); + long callerBefore = account(CALLER).getEnergyUsage(); + long originBefore = account(SOURCE).getEnergyUsage(); + long callerWindowBefore = account(CALLER).getWindowSize(ENERGY); + long originWindowBefore = account(SOURCE).getWindowSize(ENERGY); + TransactionTrace shared = execute(OUTER); + assertZeroEnergy(shared); + assertTrue(account(CALLER).getEnergyUsage() > callerBefore); + assertTrue(account(SOURCE).getEnergyUsage() > originBefore); + finish(shared); + assertEquals(callerBefore, account(CALLER).getEnergyUsage()); + assertEquals(originBefore, account(SOURCE).getEnergyUsage()); + assertEquals(callerWindowBefore, account(CALLER).getWindowSize(ENERGY)); + assertEquals(originWindowBefore, account(SOURCE).getWindowSize(ENERGY)); + assertEquals(0, shared.getReceipt().getOriginEnergyUsage()); + } + + @Test + public void zeroEnergySuccessKeepsReservationBeforeProposal() throws Exception { + VMConfig.initAllowTvmResourceOptimization(0); + dbManager.getDynamicPropertiesStore().saveAllowTvmResourceOptimization(0); + prepareEnergyWindow(); + install(SOURCE, "00"); + stake(CALLER, ENERGY, 1_000L * TRX_PRECISION); + stake(SOURCE, ENERGY, 1_000L * TRX_PRECISION); + ownEnergyWeight(CALLER, SOURCE); + install(OUTER, "00", SOURCE, 0L, 10_000_000L); + TransactionTrace trace = execute(OUTER); + assertZeroEnergy(trace); + long callerReserved = account(CALLER).getEnergyUsage(); + long originReserved = account(SOURCE).getEnergyUsage(); + assertTrue(callerReserved > 0); + assertTrue(originReserved > 0); + finish(trace); + assertEquals(callerReserved, account(CALLER).getEnergyUsage()); + assertEquals(originReserved, account(SOURCE).getEnergyUsage()); + } + + private void prepareEnergyWindow() { + dbManager.getDynamicPropertiesStore().saveEnergyFee(100); + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(100_000_000L); + } + + private void ownEnergyWeight(byte[]... addresses) { + long weight = 0; + for (byte[] address : addresses) { + weight += account(address).getFrozenV2BalanceForEnergy() / TRX_PRECISION; + } + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(weight); + } + + private void assertZeroEnergy(TransactionTrace trace) { + assertSuccess(trace); + assertEquals(0, trace.getReceipt().getEnergyUsageTotal()); + assertEquals(0, trace.getReceipt().getEnergyUsage()); + assertEquals(0, trace.getReceipt().getEnergyFee()); + } } diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/TvmOptimizationVmTestBase.java b/framework/src/test/java/org/tron/core/vm/nativecontract/TvmOptimizationVmTestBase.java index 7fec55be146..04f54b66c38 100644 --- a/framework/src/test/java/org/tron/core/vm/nativecontract/TvmOptimizationVmTestBase.java +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/TvmOptimizationVmTestBase.java @@ -72,12 +72,18 @@ protected void afterInit() { protected abstract void enableOptimization(); protected void install(byte[] address, String runtime) { + install(address, runtime, CALLER, 100L, 1_000_000L); + } + + protected void install(byte[] address, String runtime, byte[] origin, long percent, + long originEnergyLimit) { Repository root = RepositoryImpl.createRoot(StoreFactory.getInstance()); root.createAccount(address, Protocol.AccountType.Contract); root.createContract(address, new ContractCapsule(SmartContract.newBuilder() .setContractAddress(ByteString.copyFrom(address)) - .setOriginAddress(ByteString.copyFrom(CALLER)) - .setConsumeUserResourcePercent(100).setOriginEnergyLimit(1_000_000L).build())); + .setOriginAddress(ByteString.copyFrom(origin)) + .setConsumeUserResourcePercent(percent) + .setOriginEnergyLimit(originEnergyLimit).build())); root.saveCode(address, Hex.decode(runtime)); root.commit(); } From ae000d138d6af46237489aee6a760a52b42e1e02 Mon Sep 17 00:00:00 2001 From: Asuka Date: Mon, 2 Nov 2026 16:30:18 +0800 Subject: [PATCH 07/10] fix(vm): count fractional stake energy weight --- .../core/vm/repository/RepositoryImpl.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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 d49a981cf12..a2a02cfff80 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 @@ -990,6 +990,12 @@ private boolean hardenResourceCalculation() { public long calculateGlobalEnergyLimit(AccountCapsule accountCapsule) { long frozeBalance = accountCapsule.getAllFrozenBalanceForEnergy(); + // Resource queries use the fractional Stake 2.0 weight. Keep the truncated weight until + // resource optimization so earlier executions do not change how much TRX is burned. + if (VMConfig.allowTvmResourceOptimization() + && getDynamicPropertiesStore().supportUnfreezeDelay()) { + return calculateGlobalEnergyLimitV2(frozeBalance); + } if (frozeBalance < TRX_PRECISION) { return 0; } @@ -1006,6 +1012,27 @@ public long calculateGlobalEnergyLimit(AccountCapsule accountCapsule) { return (long) (energyWeight * ((double) totalEnergyLimit / totalEnergyWeight)); } + /** + * Same limit as {@code EnergyProcessor#calculateGlobalEnergyLimitV2}. Fractional suns count, + * and a balance below one TRX is not forced to zero. + */ + private long calculateGlobalEnergyLimitV2(long frozeBalance) { + long totalEnergyLimit = getDynamicPropertiesStore().getTotalEnergyCurrentLimit(); + long totalEnergyWeight = getDynamicPropertiesStore().getTotalEnergyWeight(); + if (totalEnergyWeight == 0) { + return 0; + } + if (hardenResourceCalculation()) { + return BigInteger.valueOf(frozeBalance) + .multiply(BigInteger.valueOf(totalEnergyLimit)) + .divide(BigInteger.valueOf(TRX_PRECISION) + .multiply(BigInteger.valueOf(totalEnergyWeight))) + .longValueExact(); + } + double energyWeight = (double) frozeBalance / TRX_PRECISION; + return (long) (energyWeight * ((double) totalEnergyLimit / totalEnergyWeight)); + } + public long getHeadSlot() { return getSlotByTimestampMs(getDynamicPropertiesStore().getLatestBlockHeaderTimestamp()); } From 198963e3c90c65d69bb547451529c437d180ce2d Mon Sep 17 00:00:00 2001 From: Asuka Date: Mon, 2 Nov 2026 16:30:19 +0800 Subject: [PATCH 08/10] test(vm): cover fractional stake energy weight --- .../repository/RepositoryImplHardenTest.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplHardenTest.java b/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplHardenTest.java index 6b15409edd6..2b8d9c3d252 100644 --- a/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplHardenTest.java +++ b/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplHardenTest.java @@ -13,6 +13,7 @@ import org.tron.core.Wallet; import org.tron.core.capsule.AccountCapsule; import org.tron.core.config.args.Args; +import org.tron.core.db.EnergyProcessor; import org.tron.core.store.StoreFactory; import org.tron.core.vm.config.VMConfig; import org.tron.protos.Protocol.AccountType; @@ -277,4 +278,62 @@ public void testCalculateGlobalEnergyLimitHardenedOverflowDetected() { Assert.assertThrows(ArithmeticException.class, () -> repository.calculateGlobalEnergyLimit(account)); } + + @Test + public void testResourceOptimizationKeepsFractionalEnergyWeight() { + long totalEnergyLimit = 50_000_000_000L; + long totalEnergyWeight = 2_000_000_000L; + long previousDelay = dbManager.getDynamicPropertiesStore().getUnfreezeDelayDays(); + boolean previousOptimization = VMConfig.allowTvmResourceOptimization(); + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + VMConfig.initAllowHardenResourceCalculation(0); + EnergyProcessor energyProcessor = new EnergyProcessor( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + try { + AccountCapsule partialTrx = energyAccount(500_000L); + AccountCapsule oneAndHalfTrx = energyAccount(1_500_000L); + + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + VMConfig.initAllowTvmResourceOptimization(0); + Assert.assertEquals(0L, repository.calculateGlobalEnergyLimit(partialTrx)); + Assert.assertEquals(25L, repository.calculateGlobalEnergyLimit(oneAndHalfTrx)); + + VMConfig.initAllowTvmResourceOptimization(1); + Assert.assertEquals(energyProcessor.calculateGlobalEnergyLimit(partialTrx), + repository.calculateGlobalEnergyLimit(partialTrx)); + Assert.assertEquals(energyProcessor.calculateGlobalEnergyLimit(oneAndHalfTrx), + repository.calculateGlobalEnergyLimit(oneAndHalfTrx)); + Assert.assertEquals(12L, repository.calculateGlobalEnergyLimit(partialTrx)); + Assert.assertEquals(37L, repository.calculateGlobalEnergyLimit(oneAndHalfTrx)); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + VMConfig.initAllowHardenResourceCalculation(1); + Assert.assertEquals(energyProcessor.calculateGlobalEnergyLimit(partialTrx), + repository.calculateGlobalEnergyLimit(partialTrx)); + Assert.assertEquals(12L, repository.calculateGlobalEnergyLimit(partialTrx)); + + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); + VMConfig.initAllowHardenResourceCalculation(0); + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + Assert.assertEquals(0L, repository.calculateGlobalEnergyLimit(partialTrx)); + Assert.assertEquals(25L, repository.calculateGlobalEnergyLimit(oneAndHalfTrx)); + } finally { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(previousDelay); + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + VMConfig.initAllowTvmResourceOptimization(previousOptimization ? 1 : 0); + VMConfig.initAllowHardenResourceCalculation(0); + } + } + + private AccountCapsule energyAccount(long frozeBalance) { + AccountCapsule account = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString( + Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a1abc")), + AccountType.Normal, 0L); + account.setFrozenForEnergy(frozeBalance, 0L); + return account; + } } From 2851378e8ca4390ca3bbc244703f46d6eebbb95f Mon Sep 17 00:00:00 2001 From: Asuka Date: Thu, 5 Nov 2026 18:43:07 +0800 Subject: [PATCH 09/10] fix(vm): harden resource execution and key hashing Gate seeded repository-key hashing and post-child CPU deadline checks behind ALLOW_TVM_RESOURCE_OPTIMIZATION. Mix unsigned 32-bit words with a fixed per-process seed table and retain the legacy hash for keys over 80 bytes. Cover collision families, proposal switches, long-key compatibility, and jump-destination cache recompilation and eviction. Select the proposal mode before populating TIP-945 test repository caches. --- .../org/tron/core/vm/program/Program.java | 9 + .../java/org/tron/core/vm/repository/Key.java | 44 ++- .../program/ProgramPrecompileCacheTest.java | 82 ++++++ .../org/tron/core/vm/repository/KeyTest.java | 264 ++++++++++++++++++ .../core/vm/nativecontract/Tip945Test.java | 12 +- 5 files changed, 406 insertions(+), 5 deletions(-) create mode 100644 actuator/src/test/java/org/tron/core/vm/program/ProgramPrecompileCacheTest.java create mode 100644 actuator/src/test/java/org/tron/core/vm/repository/KeyTest.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 15cd7403cb7..e2a0632c646 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 @@ -978,6 +978,11 @@ this, new DataWord(newAddress), getContractAddress(), value, DataWord.ZERO(), // Reuse the table prepared by the top-level execution. VM.play(program, OperationRegistry.getTable(isConstantCall())); createResult = program.getResult(); + + if (VMConfig.allowTvmResourceOptimization()) { + checkCPUTimeLimit(isCreate2 ? "create2" : "create"); + } + getTrace().merge(program.getTrace()); // always commit nonce this.nonce = program.nonce; @@ -1212,6 +1217,10 @@ this, new DataWord(contextAddress), VM.play(program, OperationRegistry.getTable(isConstantCall())); callResult = program.getResult(); + if (VMConfig.allowTvmResourceOptimization()) { + checkCPUTimeLimit(Op.getNameOf(msg.getOpCode())); + } + getTrace().merge(program.getTrace()); getResult().merge(callResult); // always commit nonce diff --git a/actuator/src/main/java/org/tron/core/vm/repository/Key.java b/actuator/src/main/java/org/tron/core/vm/repository/Key.java index 8639b55f4db..dc42ec9746a 100644 --- a/actuator/src/main/java/org/tron/core/vm/repository/Key.java +++ b/actuator/src/main/java/org/tron/core/vm/repository/Key.java @@ -1,7 +1,9 @@ package org.tron.core.vm.repository; +import java.security.SecureRandom; import java.util.Arrays; import org.apache.commons.lang3.ArrayUtils; +import org.tron.core.vm.config.VMConfig; public class Key { @@ -49,6 +51,46 @@ public boolean equals(Object o) { @Override public int hashCode() { - return data != null ? ArrayUtils.hashCode(data) : 0; + if (!VMConfig.allowTvmResourceOptimization()) { + return data != null ? ArrayUtils.hashCode(data) : 0; + } + return optimizedHash(data, ResourceHashHolder.SEEDS, ResourceHashHolder.LENGTH_SEED); + } + + private static class ResourceHashHolder { + + // Twenty 32-bit words cover production keys, including the variable-length reward keys. + // Keep random initialization out of the pre-proposal path. + private static final long[] SEEDS = new long[20]; + private static final long LENGTH_SEED; + + static { + SecureRandom random = new SecureRandom(); + LENGTH_SEED = random.nextLong(); + for (int i = 0; i < SEEDS.length; i++) { + SEEDS[i] = random.nextLong(); + } + } + } + + static int optimizedHash(byte[] bytes, long[] seeds, long lengthSeed) { + if (bytes.length > seeds.length * 4) { + // Preserve arbitrary-length inputs without reusing coefficients or ignoring the tail. + return ArrayUtils.hashCode(bytes); + } + long hash = lengthSeed * bytes.length; + for (int offset = 0, index = 0; offset < bytes.length; offset += 4, index++) { + int length = bytes.length - offset; + if (length > 4) { + length = 4; + } + // Every unsigned 32-bit word, including the tail, has its own coefficient. + long word = 0L; + for (int i = 0; i < length; i++) { + word |= ((long) bytes[offset + i] & 0xff) << (8 * i); + } + hash += word * seeds[index]; + } + return (int) (hash >>> 32); } } diff --git a/actuator/src/test/java/org/tron/core/vm/program/ProgramPrecompileCacheTest.java b/actuator/src/test/java/org/tron/core/vm/program/ProgramPrecompileCacheTest.java new file mode 100644 index 00000000000..9039108b5e4 --- /dev/null +++ b/actuator/src/test/java/org/tron/core/vm/program/ProgramPrecompileCacheTest.java @@ -0,0 +1,82 @@ +package org.tron.core.vm.program; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Map; +import org.apache.commons.collections4.map.LRUMap; +import org.junit.After; +import org.junit.Test; +import org.tron.common.runtime.InternalTransaction; +import org.tron.common.runtime.vm.DataWord; +import org.tron.core.vm.config.VMConfig; +import org.tron.core.vm.program.invoke.ProgramInvoke; +import org.tron.core.vm.repository.Key; +import org.tron.core.vm.repository.Repository; + +public class ProgramPrecompileCacheTest { + + @After + public void tearDown() { + VMConfig.clearLocalSnapshot(); + } + + @Test + public void cacheCanRecompileAcrossProposalActivationAndRollback() { + for (boolean enabled : new boolean[]{false, true, false, true}) { + setResourceOptimization(enabled); + ProgramPrecompile compiled = program().getProgramPrecompile(); + assertTrue(compiled.hasJumpDest(0)); + assertTrue(compiled.hasJumpDest(3)); + assertFalse(compiled.hasJumpDest(1)); + assertFalse(compiled.hasJumpDest(2)); + assertFalse(compiled.hasJumpDest(4)); + assertSame(compiled, program().getProgramPrecompile()); + } + } + + @Test + public void lruEvictsEntriesWithOldHashesWithoutExceedingCapacity() { + LRUMap cache = new LRUMap<>(4); + for (boolean enabled : new boolean[]{false, true, false, true}) { + setResourceOptimization(enabled); + // Replace every entry after each switch, including entries from the previous hash mode. + for (int value = 0; value < 16; value++) { + cache.put(cacheKey(value), value); + assertEquals(Integer.valueOf(value), cache.get(cacheKey(value))); + assertTrue(cache.size() <= 4); + } + assertEquals(4, cache.size()); + for (Map.Entry entry : cache.entrySet()) { + assertEquals(entry.getValue(), cache.get(entry.getKey(), false)); + } + } + } + + private static Program program() { + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = 0x5a; + ProgramInvoke invoke = mock(ProgramInvoke.class); + when(invoke.getContractAddress()).thenReturn(new DataWord(address)); + when(invoke.getDeposit()).thenReturn(mock(Repository.class)); + return new Program(new byte[]{0x5b, 0x60, 0x5b, 0x5b, 0}, address, invoke, + mock(InternalTransaction.class)); + } + + private static Key cacheKey(int value) { + byte[] bytes = new byte[53]; + bytes[52] = (byte) value; + return Key.create(bytes); + } + + private static void setResourceOptimization(boolean enabled) { + VMConfig.Snapshot snapshot = new VMConfig.Snapshot(); + snapshot.allowTvmResourceOptimization = enabled; + VMConfig.setLocalSnapshot(snapshot); + } +} diff --git a/actuator/src/test/java/org/tron/core/vm/repository/KeyTest.java b/actuator/src/test/java/org/tron/core/vm/repository/KeyTest.java new file mode 100644 index 00000000000..469c8306c6d --- /dev/null +++ b/actuator/src/test/java/org/tron/core/vm/repository/KeyTest.java @@ -0,0 +1,264 @@ +package org.tron.core.vm.repository; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.tron.core.vm.config.VMConfig; + +public class KeyTest { + + @Before + public void setUp() { + setResourceOptimization(false); + } + + @After + public void tearDown() { + VMConfig.clearLocalSnapshot(); + } + + @Test + public void legacyHashIsPreservedBeforeProposal() { + assertEquals(ArrayUtils.hashCode(new byte[0]), Key.create(null).hashCode()); + for (int length = 0; length <= 257; length++) { + byte[] bytes = payload(length); + assertEquals(ArrayUtils.hashCode(bytes), Key.create(bytes).hashCode()); + } + } + + @Test + public void keyDoesNotImplementComparable() { + assertFalse(Comparable.class.isAssignableFrom(Key.class)); + } + + @Test + public void proposalEnablesOptimizedHash() { + setResourceOptimization(true); + for (int length = 1; length <= 257; length++) { + byte[] bytes = payload(length); + if (Key.create(bytes).hashCode() != ArrayUtils.hashCode(bytes)) { + return; + } + } + throw new AssertionError("The enabled proposal still uses the legacy hash for every sample"); + } + + @Test + public void hashMapRoundTripsBothModesAndPartialWords() { + int[] lengths = {0, 1, 2, 3, 4, 7, 8, 15, 16, 20, 21, 31, 32, 33, 52, 53, + 63, 64, 65, 75, 76, 77, 78, 79, 80, 81, 127, 128, 129, 255, 256, 257, 1025}; + for (boolean enabled : new boolean[]{false, true}) { + setResourceOptimization(enabled); + Map map = new HashMap<>(); + for (int length : lengths) { + Key key = Key.create(payload(length)); + map.put(key, length); + assertEquals(key, key.clone()); + assertEquals(key.hashCode(), key.clone().hashCode()); + } + for (int length : lengths) { + Key again = Key.create(payload(length)); + assertEquals(Integer.valueOf(length), map.get(again)); + assertEquals(again.hashCode(), Key.create(payload(length)).hashCode()); + } + assertEquals(Key.create(null), Key.create(new byte[0])); + assertEquals(Integer.valueOf(0), map.get(Key.create(null))); + } + } + + @Test + public void proposalFlagIsReadAtHashTimeWithoutChangingEquality() { + byte[] bytes = payload(53); + Key beforeProposal = Key.create(bytes); + int legacyHash = beforeProposal.hashCode(); + + setResourceOptimization(true); + Key afterProposal = Key.create(bytes); + int optimizedHash = afterProposal.hashCode(); + assertEquals(beforeProposal, afterProposal); + assertEquals(afterProposal, beforeProposal); + assertEquals(optimizedHash, beforeProposal.hashCode()); + assertEquals(beforeProposal, beforeProposal.clone()); + assertEquals(optimizedHash, beforeProposal.clone().hashCode()); + + setResourceOptimization(false); + assertEquals(legacyHash, beforeProposal.hashCode()); + assertEquals(legacyHash, afterProposal.hashCode()); + assertEquals(legacyHash, afterProposal.clone().hashCode()); + assertEquals(beforeProposal, Key.create(bytes)); + + setResourceOptimization(true); + assertEquals(optimizedHash, beforeProposal.hashCode()); + assertEquals(optimizedHash, afterProposal.hashCode()); + } + + @Test + public void finalWordLowBitsDoNotCollapseIntoOneBucket() { + // The old 64-bit recurrence left these bytes unmixed before truncating to the high half. + int count = 1 << 16; + for (int seed = 0; seed < 16; seed++) { + TestHash hash = new TestHash(seed); + Set hashes = new HashSet<>(); + byte[] slot = new byte[32]; + for (int value = 0; value < count; value++) { + slot[24] = (byte) value; + slot[25] = (byte) (value >>> 8); + hashes.add(hash.hash(slot)); + } + // Hash collisions are legal; reject a collapsed family rather than require uniqueness. + assertTrue("seed=" + seed + ", hashes=" + hashes.size(), hashes.size() > count * 99 / 100); + } + } + + @Test + public void evenHighBitFlipsDoNotCancelAcrossWords() { + for (int seed = 0; seed < 16; seed++) { + TestHash hash = new TestHash(seed); + Set hashes = new HashSet<>(); + for (int mask = 0; mask < 16; mask++) { + if ((Integer.bitCount(mask) & 1) != 0) { + continue; + } + byte[] slot = new byte[32]; + for (int word = 0; word < 4; word++) { + if ((mask & (1 << word)) != 0) { + slot[word * 8 + 7] = (byte) 0x80; + } + } + hashes.add(hash.hash(slot)); + } + // All eight slots used to collide for every odd 64-bit coefficient set. + assertTrue("seed=" + seed, hashes.size() > 1); + } + } + + @Test + public void everyWordAndTailAffectsHash() { + for (int seed = 0; seed < 4; seed++) { + TestHash hash = new TestHash(seed); + for (int length : new int[]{16, 19, 21, 23, 32, 42, 43, 46, 53, 65, 75, 76, 79, 80}) { + for (int offset = 0; offset < length; offset += 4) { + byte[] bytes = payload(length); + Set hashes = new HashSet<>(); + for (int value = 0; value < 256; value++) { + bytes[offset] = (byte) value; + hashes.add(hash.hash(bytes)); + } + assertTrue("seed=" + seed + ", length=" + length + ", offset=" + offset, + hashes.size() > 250); + } + } + } + } + + @Test + public void lengthSeparatesZeroPaddedKeys() { + for (int seed = 0; seed < 16; seed++) { + TestHash hash = new TestHash(seed); + Set hashes = new HashSet<>(); + for (int length = 1; length <= 129; length++) { + byte[] bytes = new byte[length]; + bytes[0] = 1; + hashes.add(hash.hash(bytes)); + } + assertTrue("seed=" + seed, hashes.size() > 125); + } + } + + @Test + public void longKeysUseLegacyHashAndIncludeTheWholeTail() { + setResourceOptimization(true); + TestHash hash = new TestHash(42); + for (int length : new int[]{81, 82, 83, 84, 128, 129, 257, 1025, 4097}) { + byte[] bytes = payload(length); + Key key = Key.create(bytes); + int original = key.hashCode(); + assertEquals(ArrayUtils.hashCode(bytes), original); + assertEquals(original, hash.hash(bytes)); + bytes[length - 1] ^= 1; + Key changedTail = Key.create(bytes); + assertEquals(ArrayUtils.hashCode(bytes), changedTail.hashCode()); + assertNotEquals(original, changedTail.hashCode()); + assertEquals(original, key.hashCode()); + } + } + + @Test + public void transientStorageChildCommitAndDiscardWorkInBothModes() { + byte[] address = payload(32); + for (boolean enabled : new boolean[]{false, true}) { + setResourceOptimization(enabled); + Repository root = new RepositoryImpl(null, null); + Repository child = root.newRepositoryChild(); + for (int value = 0; value < 512; value++) { + byte[] slot = new byte[32]; + slot[24] = (byte) value; + slot[25] = (byte) (value >>> 8); + byte[] data = payload(32); + data[0] = (byte) value; + data[1] = (byte) (value >>> 8); + child.updateTransientStorageValue(address, slot, data); + assertArrayEquals(data, child.getTransientStorageValue(address, slot)); + assertNull(root.getTransientStorageValue(address, slot)); + } + child.commit(); + for (int value = 0; value < 512; value++) { + byte[] slot = new byte[32]; + slot[24] = (byte) value; + slot[25] = (byte) (value >>> 8); + byte[] data = payload(32); + data[0] = (byte) value; + data[1] = (byte) (value >>> 8); + assertArrayEquals(data, root.getTransientStorageValue(address, slot)); + Repository discarded = root.newRepositoryChild(); + discarded.updateTransientStorageValue(address, slot, new byte[32]); + assertArrayEquals(data, root.getTransientStorageValue(address, slot)); + } + } + } + + private static void setResourceOptimization(boolean enabled) { + VMConfig.Snapshot snapshot = new VMConfig.Snapshot(); + snapshot.allowTvmResourceOptimization = enabled; + VMConfig.setLocalSnapshot(snapshot); + } + + private static class TestHash { + + private final long[] seeds = new long[20]; + private final long lengthSeed; + + private TestHash(long seed) { + Random random = new Random(seed); + lengthSeed = random.nextLong(); + for (int i = 0; i < seeds.length; i++) { + seeds[i] = random.nextLong(); + } + } + + private int hash(byte[] bytes) { + return Key.optimizedHash(bytes, seeds, lengthSeed); + } + } + + private static byte[] payload(int length) { + byte[] data = new byte[length]; + for (int i = 0; i < length; i++) { + data[i] = (byte) (i * 17 + 3); + } + return data; + } +} diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java index c66918e7977..ca69ca7a361 100644 --- a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java @@ -61,11 +61,16 @@ public static void init() { @Before public void setup() { + setup(1); + } + + private void setup(long resourceOptimization) { + // Select the proposal before creating repository keys, as transaction setup does. VMConfig.setGlobalSnapshot(new VMConfig.Snapshot()); ConfigLoader.disable = true; VMConfig.initAllowTvmFreezeV2(1); VMConfig.initAllowTvmSelfdestructRestriction(1); - VMConfig.initAllowTvmResourceOptimization(1); + VMConfig.initAllowTvmResourceOptimization(resourceOptimization); dbManager.getDynamicPropertiesStore().saveAllowNewResourceModel(1); dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); dbManager.getDynamicPropertiesStore().saveAllowTvmResourceOptimization(0); @@ -262,9 +267,9 @@ public void selfBeneficiaryAndRevertedTransferLeaveParentWeightsUnchanged() { @Test public void disabledFlagAndNonTransferredBalancesLeaveWeightsUnchanged() { + setup(0); stake(OWNER, BANDWIDTH, 1_600_000); stake(BENEFICIARY, BANDWIDTH, 2_600_000); - VMConfig.initAllowTvmResourceOptimization(0); long[] before = weights(); program(repo, OWNER).suicide2(new DataWord(BENEFICIARY)); assertArrayEquals(before, weights()); @@ -349,9 +354,8 @@ public void actualOpcodeEnergyAndStaticChecksPrecedeCountRejection() { public void independentActivationPreservesVotingStateAndWeightBoundaries() { for (long power : new long[]{0, 1}) { for (long resource : new long[]{0, 1}) { - setup(); + setup(resource); VMConfig.initAllowTvmVotePowerOptimization(power); - VMConfig.initAllowTvmResourceOptimization(resource); stake(OWNER, BANDWIDTH, 1_600_000); stake(BENEFICIARY, BANDWIDTH, 2_600_000); Program p = program(repo, OWNER); From 65298142a854a7e54ca2de267afcb9fd5ee9610d Mon Sep 17 00:00:00 2001 From: Asuka Date: Thu, 5 Nov 2026 20:44:13 +0800 Subject: [PATCH 10/10] fix(vm): release precompile execution references Clear repository and ProgramResult references in a finally block after precompiled-contract execution to avoid retaining transaction state. Cover successful and failed returns, runtime exceptions, timeouts, stored ProgramResult exceptions, and instance reuse for regular and constant calls with resource optimization enabled and disabled. --- .../org/tron/core/vm/program/Program.java | 8 +- ...ProgramPrecompiledContractCleanupTest.java | 226 ++++++++++++++++++ 2 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 actuator/src/test/java/org/tron/core/vm/program/ProgramPrecompiledContractCleanupTest.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 e2a0632c646..869a9cc6e41 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 @@ -1814,7 +1814,13 @@ public void callToPrecompiledAddress(MessageCall msg, contract.setResult(this.result); contract.setConstantCall(isConstantCall()); contract.setVmShouldEndInUs(getVmShouldEndInUs()); - Pair out = contract.execute(data); + Pair out; + try { + out = contract.execute(data); + } finally { + contract.setRepository(null); + contract.setResult(null); + } if (out.getLeft()) { // success this.refundEnergy(msg.getEnergy().longValue() - requiredEnergy, CALL_PRE_COMPILED); diff --git a/actuator/src/test/java/org/tron/core/vm/program/ProgramPrecompiledContractCleanupTest.java b/actuator/src/test/java/org/tron/core/vm/program/ProgramPrecompiledContractCleanupTest.java new file mode 100644 index 00000000000..b9138b11adc --- /dev/null +++ b/actuator/src/test/java/org/tron/core/vm/program/ProgramPrecompiledContractCleanupTest.java @@ -0,0 +1,226 @@ +package org.tron.core.vm.program; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collection; +import java.util.function.Function; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.tron.common.runtime.InternalTransaction; +import org.tron.common.runtime.ProgramResult; +import org.tron.common.runtime.vm.DataWord; +import org.tron.core.vm.MessageCall; +import org.tron.core.vm.Op; +import org.tron.core.vm.PrecompiledContracts.PrecompiledContract; +import org.tron.core.vm.config.VMConfig; +import org.tron.core.vm.program.Program.OutOfTimeException; +import org.tron.core.vm.program.invoke.ProgramInvoke; +import org.tron.core.vm.repository.Repository; + +@RunWith(Parameterized.class) +public class ProgramPrecompiledContractCleanupTest { + + private static final long CALL_ENERGY = 1000; + private static final long REQUIRED_ENERGY = 20; + private static final byte[] INPUT = new DataWord(7).getData(); + private static final byte[] OUTPUT = new DataWord(42).getData(); + + private final boolean constantCall; + private final boolean resourceOptimization; + private Repository child; + private Program program; + private ProgramResult result; + + @Parameters(name = "constantCall={0}, resourceOptimization={1}") + public static Collection parameters() { + return Arrays.asList(new Object[][]{ + {false, false}, {false, true}, {true, false}, {true, true} + }); + } + + public ProgramPrecompiledContractCleanupTest(boolean constantCall, boolean resourceOptimization) { + this.constantCall = constantCall; + this.resourceOptimization = resourceOptimization; + } + + @Before + public void setUp() { + VMConfig.Snapshot snapshot = new VMConfig.Snapshot(); + snapshot.allowTvmResourceOptimization = resourceOptimization; + VMConfig.setLocalSnapshot(snapshot); + newExecutionContext(); + } + + @After + public void tearDown() { + VMConfig.clearLocalSnapshot(); + } + + @Test + public void successfulReturnClearsReferencesAndCommitsChild() { + PrecompiledContract contract = returning(true); + + program.callToPrecompiledAddress(message(), contract); + + assertReferencesCleared(contract); + verify(child).commit(); + assertSame(result, program.getResult()); + assertNull(result.getException()); + assertEquals(DataWord.ONE(), program.getStack().pop()); + assertEquals(REQUIRED_ENERGY, result.getEnergyUsed()); + assertArrayEquals(OUTPUT, + program.getReturnDataBufferData(DataWord.ZERO(), new DataWord(OUTPUT.length))); + assertArrayEquals(OUTPUT, program.memoryChunk(64, OUTPUT.length)); + } + + @Test + public void failedReturnClearsReferencesWithoutCommittingChild() { + PrecompiledContract contract = returning(false); + + program.callToPrecompiledAddress(message(), contract); + + assertReferencesCleared(contract); + verify(child, never()).commit(); + assertSame(result, program.getResult()); + assertNull(result.getException()); + assertEquals(DataWord.ZERO(), program.getStack().pop()); + assertEquals(CALL_ENERGY, result.getEnergyUsed()); + } + + @Test + public void runtimeExceptionClearsReferencesAndPropagates() { + IllegalStateException failure = new IllegalStateException("precompile failed"); + PrecompiledContract contract = new InspectingContract(context -> { + throw failure; + }); + + assertSame(failure, assertThrows(IllegalStateException.class, + () -> program.callToPrecompiledAddress(message(), contract))); + + assertReferencesCleared(contract); + verify(child, never()).commit(); + assertSame(result, program.getResult()); + } + + @Test + public void timeoutClearsReferencesAndPropagates() { + OutOfTimeException failure = new OutOfTimeException("precompile timed out"); + PrecompiledContract contract = new InspectingContract(context -> { + throw failure; + }); + + assertSame(failure, assertThrows(OutOfTimeException.class, + () -> program.callToPrecompiledAddress(message(), contract))); + + assertReferencesCleared(contract); + verify(child, never()).commit(); + assertSame(result, program.getResult()); + } + + @Test + public void failedReturnPreservesExceptionStoredInProgramResult() { + IllegalStateException failure = new IllegalStateException("recorded precompile failure"); + PrecompiledContract contract = new InspectingContract(context -> { + context.getResult().setException(failure); + return Pair.of(false, new byte[0]); + }); + + assertSame(failure, assertThrows(IllegalStateException.class, + () -> program.callToPrecompiledAddress(message(), contract))); + + assertReferencesCleared(contract); + verify(child, never()).commit(); + assertSame(result, program.getResult()); + assertSame(failure, result.getException()); + assertEquals(DataWord.ZERO(), program.getStack().pop()); + } + + @Test + public void reusedPrecompileReceivesFreshExecutionReferences() { + PrecompiledContract contract = returning(true); + Repository firstChild = child; + ProgramResult firstResult = result; + program.callToPrecompiledAddress(message(), contract); + assertReferencesCleared(contract); + verify(firstChild).commit(); + + newExecutionContext(); + assertNotSame(firstChild, child); + assertNotSame(firstResult, result); + program.callToPrecompiledAddress(message(), contract); + + assertReferencesCleared(contract); + verify(child).commit(); + assertEquals(DataWord.ONE(), program.getStack().pop()); + assertEquals(REQUIRED_ENERGY, result.getEnergyUsed()); + } + + private void newExecutionContext() { + Repository repository = mock(Repository.class); + child = mock(Repository.class); + when(repository.newRepositoryChild()).thenReturn(child); + ProgramInvoke invoke = mock(ProgramInvoke.class); + when(invoke.getDeposit()).thenReturn(repository); + when(invoke.getContractAddress()).thenReturn(new DataWord(123)); + when(invoke.getEnergyLimit()).thenReturn(10000L); + when(invoke.isConstantCall()).thenReturn(constantCall); + program = new Program(new byte[0], new byte[21], invoke, mock(InternalTransaction.class)); + program.memorySave(0, INPUT); + // Mirror the caller reserving the message's energy before invoking the precompile. + program.spendEnergy(CALL_ENERGY, "precompile call"); + result = program.getResult(); + } + + private static MessageCall message() { + return new MessageCall(Op.CALL, new DataWord(CALL_ENERGY), new DataWord(4), + DataWord.ZERO(), DataWord.ZERO(), new DataWord(INPUT.length), + new DataWord(64), new DataWord(OUTPUT.length), DataWord.ZERO(), false); + } + + private PrecompiledContract returning(boolean success) { + return new InspectingContract(context -> Pair.of(success, OUTPUT)); + } + + private static void assertReferencesCleared(PrecompiledContract contract) { + assertNull(contract.getDeposit()); + assertNull(contract.getResult()); + } + + private class InspectingContract extends PrecompiledContract { + + private final Function> execution; + + private InspectingContract(Function> execution) { + this.execution = execution; + } + + @Override + public long getEnergyForData(byte[] data) { + return REQUIRED_ENERGY; + } + + @Override + public Pair execute(byte[] data) { + assertSame(child, getDeposit()); + assertSame(result, getResult()); + assertEquals(constantCall, isConstantCall()); + assertArrayEquals(INPUT, data); + return execution.apply(this); + } + } +}