From 9b60a5fc52fc13793eb166202368916242bdb48d Mon Sep 17 00:00:00 2001 From: Asuka Date: Tue, 27 Oct 2026 15:50:25 +0800 Subject: [PATCH 1/4] feat(vm): optimize voting power state management --- .../org/tron/core/utils/ProposalUtil.java | 18 +- .../org/tron/core/vm/config/ConfigLoader.java | 1 + .../nativecontract/VoteWitnessProcessor.java | 6 + .../org/tron/core/vm/program/Program.java | 24 +- .../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/Tip944Test.java | 275 ++++++++++++++++++ 9 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.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 acf30f4fc06..2d5818f17af 100644 --- a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java @@ -1014,6 +1014,21 @@ public static void validator(DynamicPropertiesStore dynamicPropertiesStore, } break; } + case ALLOW_TVM_VOTE_POWER_OPTIMIZATION: { + if (!forkController.pass(ForkBlockVersionEnum.VERSION_4_8_3)) { + throw new ContractValidateException( + "Bad chain parameter id [ALLOW_TVM_VOTE_POWER_OPTIMIZATION]"); + } + if (dynamicPropertiesStore.getAllowTvmVotePowerOptimization() == 1) { + throw new ContractValidateException( + "[ALLOW_TVM_VOTE_POWER_OPTIMIZATION] has been valid, no need to propose again"); + } + if (value != 1) { + throw new ContractValidateException( + "This value[ALLOW_TVM_VOTE_POWER_OPTIMIZATION] is only allowed to be 1"); + } + break; + } default: break; } @@ -1106,7 +1121,8 @@ public enum ProposalType { // current value, value range ALLOW_OPTIMIZE_TVM_STORAGE(99), // 0, 1 ALLOW_STRICT_ECDSA_VALIDATION(100), // 0, 1 CLOSE_EXCHANGE(101), // 0, 1, 2 - ALLOW_OPTIMIZE_TVM_STORAGE_KEY(102); // 0, 1 + ALLOW_OPTIMIZE_TVM_STORAGE_KEY(102), // 0, 1 + ALLOW_TVM_VOTE_POWER_OPTIMIZATION(103); // 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 526e97672b3..c364ede87d1 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 @@ -52,6 +52,7 @@ public static void load(StoreFactory storeFactory, boolean isolate) { snapshot.allowHardenResourceCalculation = ds.getAllowHardenResourceCalculation() == 1; snapshot.allowStrictEcdsaValidation = ds.getAllowStrictEcdsaValidation() == 1; snapshot.allowOptimizeTvmStorageKey = ds.allowOptimizeTvmStorageKey(); + snapshot.allowTvmVotePowerOptimization = ds.getAllowTvmVotePowerOptimization() == 1; if (isolate) { VMConfig.setLocalSnapshot(snapshot); } else { diff --git a/actuator/src/main/java/org/tron/core/vm/nativecontract/VoteWitnessProcessor.java b/actuator/src/main/java/org/tron/core/vm/nativecontract/VoteWitnessProcessor.java index 8e17ffe8b13..c5b28281f1b 100644 --- a/actuator/src/main/java/org/tron/core/vm/nativecontract/VoteWitnessProcessor.java +++ b/actuator/src/main/java/org/tron/core/vm/nativecontract/VoteWitnessProcessor.java @@ -17,6 +17,7 @@ import org.tron.core.capsule.VotesCapsule; import org.tron.core.exception.ContractExeException; import org.tron.core.exception.ContractValidateException; +import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.nativecontract.param.VoteWitnessParam; import org.tron.core.vm.repository.Repository; import org.tron.core.vm.utils.VoteRewardUtil; @@ -41,6 +42,11 @@ public void execute(VoteWitnessParam param, Repository repo) throws ContractExeE VoteRewardUtil.withdrawReward(ownerAddress, repo); AccountCapsule accountCapsule = repo.getAccount(ownerAddress); + if (VMConfig.allowTvmVotePowerOptimization() + && repo.getDynamicPropertiesStore().supportAllowNewResourceModel() + && accountCapsule.oldTronPowerIsNotInitialized()) { + accountCapsule.initializeOldTronPower(); + } VotesCapsule votesCapsule = repo.getVotes(ownerAddress); if (votesCapsule == null) { 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 2effc82b68d..4ed70964243 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 @@ -617,6 +617,12 @@ private void transferDelegatedResourceToInheritor(byte[] ownerAddr, byte[] inher ownerCapsule.getAccountResource().getFrozenBalanceForEnergy().getFrozenBalance(); repo.addTotalEnergyWeight(-frozenBalanceForEnergyOfOwner / TRX_PRECISION); + if (optimizeVotePowerState(repo) + && (frozenBalanceForBandwidthOfOwner > 0 || frozenBalanceForEnergyOfOwner > 0)) { + ownerCapsule.invalidateOldTronPower(); + repo.updateAccount(ownerAddr, ownerCapsule); + } + // transfer all kinds of frozen balance to BlackHole repo.addBalance(inheritorAddr, frozenBalanceForBandwidthOfOwner + frozenBalanceForEnergyOfOwner); @@ -631,6 +637,15 @@ private long transferFrozenV2BalanceToInheritor(byte[] ownerAddr, byte[] inherit AccountCapsule inheritorCapsule = repo.getAccount(inheritorAddr); long now = repo.getHeadSlot(); + if (optimizeVotePowerState(repo) + && ownerCapsule.getFrozenV2List().stream().anyMatch(frozen -> frozen.getAmount() > 0)) { + if (inheritorCapsule.oldTronPowerIsNotInitialized()) { + inheritorCapsule.initializeOldTronPower(); + } + // Invalidate before returning, including accounts queued for deletion at finalization. + ownerCapsule.invalidateOldTronPower(); + } + // transfer frozen resource ownerCapsule.getFrozenV2List().stream() .filter(freezeV2 -> freezeV2.getAmount() > 0) @@ -703,6 +718,11 @@ private void clearOwnerFreezeV2(AccountCapsule ownerCapsule) { ownerCapsule.clearUnfrozenV2(); } + private boolean optimizeVotePowerState(Repository repo) { + return VMConfig.allowTvmVotePowerOptimization() + && repo.getDynamicPropertiesStore().supportAllowNewResourceModel(); + } + private void withdrawRewardAndCancelVote(byte[] owner, Repository repo) { VoteRewardUtil.withdrawReward(owner, repo); @@ -716,7 +736,9 @@ private void withdrawRewardAndCancelVote(byte[] owner, Repository repo) { votesCapsule.clearNewVotes(); } ownerCapsule.clearVotes(); - ownerCapsule.setOldTronPower(0); + if (!optimizeVotePowerState(repo)) { + ownerCapsule.setOldTronPower(0); + } repo.updateVotes(owner, votesCapsule); } try { 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 ba1da5cf79d..920a43929aa 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -267,6 +267,9 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking private static final byte[] ALLOW_OPTIMIZE_TVM_STORAGE_KEY = "ALLOW_OPTIMIZE_TVM_STORAGE_KEY".getBytes(); + private static final byte[] ALLOW_TVM_VOTE_POWER_OPTIMIZATION = + "ALLOW_TVM_VOTE_POWER_OPTIMIZATION".getBytes(); + @Autowired private DynamicPropertiesStore(@Value("properties") String dbName) { super(dbName); @@ -3149,6 +3152,21 @@ public boolean allowOptimizeTvmStorageKey() { return getAllowOptimizeTvmStorageKey() == 1L; } + public long getAllowTvmVotePowerOptimization() { + return Optional.ofNullable(getUnchecked(ALLOW_TVM_VOTE_POWER_OPTIMIZATION)) + .map(BytesCapsule::getData) + .map(ByteArray::toLong) + .orElse(0L); + } + + public void saveAllowTvmVotePowerOptimization(long value) { + this.put(ALLOW_TVM_VOTE_POWER_OPTIMIZATION, new BytesCapsule(ByteArray.fromLong(value))); + } + + public boolean allowTvmVotePowerOptimization() { + return getAllowTvmVotePowerOptimization() == 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 04e1bf80fba..0a9b5f8b2cd 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 @@ -46,6 +46,7 @@ public static class Snapshot { public boolean allowHardenResourceCalculation; public boolean allowStrictEcdsaValidation; public boolean allowOptimizeTvmStorageKey; + public boolean allowTvmVotePowerOptimization; } // HEAD / block-processing config, written by the consensus path; read by everyone with no @@ -204,6 +205,10 @@ public static void initAllowOptimizeTvmStorageKey(long allow) { globalSnapshot.allowOptimizeTvmStorageKey = allow == 1; } + public static void initAllowTvmVotePowerOptimization(long allow) { + globalSnapshot.allowTvmVotePowerOptimization = allow == 1; + } + public static void initAllowStrictEcdsaValidation(long allow) { globalSnapshot.allowStrictEcdsaValidation = allow == 1; } @@ -320,6 +325,10 @@ public static boolean allowOptimizeTvmStorageKey() { return current().allowOptimizeTvmStorageKey; } + public static boolean allowTvmVotePowerOptimization() { + return current().allowTvmVotePowerOptimization; + } + 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 a90a98f86cb..0434cffd194 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -1698,6 +1698,11 @@ public Protocol.ChainParameters getChainParameters() { .setValue(dbManager.getDynamicPropertiesStore().getAllowOptimizeTvmStorageKey()) .build()); + builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() + .setKey("getAllowTvmVotePowerOptimization") + .setValue(dbManager.getDynamicPropertiesStore().getAllowTvmVotePowerOptimization()) + .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 bb232419210..c8643ec0e6d 100644 --- a/framework/src/main/java/org/tron/core/consensus/ProposalService.java +++ b/framework/src/main/java/org/tron/core/consensus/ProposalService.java @@ -430,6 +430,11 @@ public static boolean process(Manager manager, ProposalCapsule proposalCapsule) .saveAllowOptimizeTvmStorageKey(entry.getValue()); break; } + case ALLOW_TVM_VOTE_POWER_OPTIMIZATION: { + manager.getDynamicPropertiesStore() + .saveAllowTvmVotePowerOptimization(entry.getValue()); + break; + } default: find = false; break; diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java new file mode 100644 index 00000000000..2afbecb96ae --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java @@ -0,0 +1,275 @@ +package org.tron.core.vm.nativecontract; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +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.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 Tip944Test 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.initAllowTvmVotePowerOptimization(1); + dbManager.getDynamicPropertiesStore().saveAllowNewResourceModel(1); + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowTvmVotePowerOptimization(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().getAllowTvmVotePowerOptimization()); + assertThrows(ContractValidateException.class, () -> ProposalUtil.validator( + dbManager.getDynamicPropertiesStore(), fork, 103, 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, 103, value)); + } + ProposalUtil.validator(dbManager.getDynamicPropertiesStore(), fork, 103, 1); + assertTrue(ProposalService.process(dbManager, new ProposalCapsule( + Protocol.Proposal.newBuilder().putParameters(103, 1).build()))); + assertEquals(1, dbManager.getDynamicPropertiesStore().getAllowTvmVotePowerOptimization()); + assertThrows(ContractValidateException.class, () -> ProposalUtil.validator( + dbManager.getDynamicPropertiesStore(), fork, 103, 1)); + ConfigLoader.disable = false; + ConfigLoader.load(StoreFactory.getInstance(), false); + assertTrue(VMConfig.allowTvmVotePowerOptimization()); + dbManager.getDynamicPropertiesStore().saveAllowTvmVotePowerOptimization(0); + ConfigLoader.load(StoreFactory.getInstance(), true); + assertFalse(VMConfig.allowTvmVotePowerOptimization()); + VMConfig.clearLocalSnapshot(); + assertTrue(VMConfig.allowTvmVotePowerOptimization()); + } + + private boolean vote(Repository repository, byte[] owner, long amount) { + Program p = program(repository, owner); + p.memorySave(0, new DataWord(1).getData()); + p.memorySave(32, new DataWord(WITNESS).getData()); + p.memorySave(64, new DataWord(1).getData()); + p.memorySave(96, new DataWord(amount).getData()); + return p.voteWitness(0, 1, 64, 1); + } + + @Test + public void initializesVotingAndPreservesSnapshot() { + stake(OWNER, BANDWIDTH, 3 * TRX_PRECISION); + assertTrue(vote(repo, OWNER, 3)); + assertEquals(3 * TRX_PRECISION, legacy(OWNER)); + stake(OWNER, ENERGY, 2 * TRX_PRECISION); + assertFalse(vote(repo, OWNER, 4)); + assertEquals(3 * TRX_PRECISION, legacy(OWNER)); + } + + @Test + public void zeroResourceInitializationUsesInvalidSentinelAndExplicitPower() { + stake(OWNER, TRON_POWER, 2 * TRX_PRECISION); + assertTrue(vote(repo, OWNER, 2)); + assertEquals(-1, legacy(OWNER)); + stake(OWNER, ENERGY, 4 * TRX_PRECISION); + assertFalse(vote(repo, OWNER, 3)); + assertEquals(-1, legacy(OWNER)); + } + + @Test + public void votingIsGatedAndFailedOrNestedExecutionDoesNotPersistInitialization() { + stake(OWNER, BANDWIDTH, 2 * TRX_PRECISION); + VMConfig.initAllowTvmVotePowerOptimization(0); + assertTrue(vote(repo, OWNER, 2)); + assertEquals(0, legacy(OWNER)); + VMConfig.initAllowTvmVotePowerOptimization(1); + assertFalse(vote(repo, OWNER, 3)); + assertEquals(0, legacy(OWNER)); + Repository child = repo.newRepositoryChild(); + assertTrue(vote(child, OWNER, 2)); + assertEquals(2 * TRX_PRECISION, child.getAccount(OWNER).getInstance().getOldTronPower()); + assertEquals(0, legacy(OWNER)); + dbManager.getDynamicPropertiesStore().saveAllowNewResourceModel(0); + assertTrue(vote(repo, OWNER, 2)); + assertEquals(0, legacy(OWNER)); + } + + @Test + public void initializesBeneficiaryBeforeReceiptAndRetiresSource() { + for (boolean deleted : new boolean[]{false, true}) { + for (long prior : new long[]{0, 7 * TRX_PRECISION, -1}) { + setup(); + legacy(BENEFICIARY, prior); + legacy(OWNER, 4 * TRX_PRECISION); + stake(OWNER, BANDWIDTH, 2 * TRX_PRECISION); + stake(OWNER, TRON_POWER, TRX_PRECISION); + stake(BENEFICIARY, ENERGY, TRX_PRECISION); + Program p = program(repo, OWNER); + if (deleted) { + p.suicide(new DataWord(BENEFICIARY)); + assertEquals(1, p.getResult().getDeleteAccounts().size()); + } else { + p.suicide2(new DataWord(BENEFICIARY)); + assertTrue(p.getResult().getDeleteAccounts().isEmpty()); + } + assertEquals(prior == 0 ? TRX_PRECISION : prior, legacy(BENEFICIARY)); + assertEquals(-1, legacy(OWNER)); + assertFalse(vote(repo, OWNER, 1)); + assertTrue(vote(repo, BENEFICIARY, prior == -1 ? 1 : 2)); + } + } + } + + @Test + public void recipientWithoutLegacyStakeIsInvalidatedAndTransferIsForkGated() { + stake(OWNER, ENERGY, 2 * TRX_PRECISION); + program(repo, OWNER).suicide2(new DataWord(BENEFICIARY)); + assertEquals(-1, legacy(BENEFICIARY)); + assertFalse(vote(repo, BENEFICIARY, 1)); + setup(); + VMConfig.initAllowTvmVotePowerOptimization(0); + stake(OWNER, ENERGY, 2 * TRX_PRECISION); + program(repo, OWNER).suicide2(new DataWord(BENEFICIARY)); + assertEquals(0, legacy(BENEFICIARY)); + assertTrue(vote(repo, BENEFICIARY, 2)); + } + + @Test + public void stakeOneRedemptionInvalidatesWithOrWithoutVotes() { + for (boolean votes : new boolean[]{false, true}) { + setup(); + VMConfig.initAllowTvmFreeze(1); + VMConfig.initAllowTvmVote(1); + AccountCapsule owner = repo.getAccount(OWNER); + owner.setFrozenForBandwidth(2 * TRX_PRECISION, 0); + owner.setOldTronPower(2 * TRX_PRECISION); + if (votes) { + owner.addVotes(ByteString.copyFrom(WITNESS), 2); + } + repo.updateAccount(OWNER, owner); + program(repo, OWNER).suicide2(new DataWord(BENEFICIARY)); + assertEquals(-1, legacy(OWNER)); + assertEquals(0, repo.getAccount(OWNER).getTronPower()); + assertEquals(2 * TRX_PRECISION, repo.getAccount(BENEFICIARY).getBalance()); + } + } + + @Test + public void selfBeneficiaryAndZeroBalancesDoNotRetireLegacyState() { + stake(OWNER, BANDWIDTH, TRX_PRECISION); + legacy(OWNER, TRX_PRECISION); + program(repo, OWNER).suicide2(new DataWord(OWNER)); + assertEquals(TRX_PRECISION, legacy(OWNER)); + setup(); + legacy(OWNER, TRX_PRECISION); + program(repo, OWNER).suicide2(new DataWord(BENEFICIARY)); + assertEquals(TRX_PRECISION, legacy(OWNER)); + } + + @Test + public void blackHoleReceiptAndTransferRollbackUseActualExecutionState() { + stake(OWNER, ENERGY, TRX_PRECISION); + byte[] blackHole = repo.getBlackHoleAddress(); + program(repo, OWNER).suicide(new DataWord(OWNER)); + assertEquals(-1, legacy(OWNER)); + assertEquals(TRX_PRECISION, repo.getAccount(blackHole).getFrozenV2BalanceForEnergy()); + setup(); + stake(OWNER, ENERGY, TRX_PRECISION); + Repository child = repo.newRepositoryChild(); + program(child, OWNER).suicide2(new DataWord(BENEFICIARY)); + assertEquals(-1, child.getAccount(OWNER).getInstance().getOldTronPower()); + assertEquals(0, legacy(OWNER)); + assertEquals(0, repo.getAccount(BENEFICIARY).getFrozenV2BalanceForEnergy()); + } +} From fb89e07ba32dd947a617ec8fe5deca4b5b562df9 Mon Sep 17 00:00:00 2001 From: Asuka Date: Tue, 27 Oct 2026 16:51:17 +0800 Subject: [PATCH 2/4] test(vm): cover voting state cleanup and transactional rollback --- .../core/vm/nativecontract/Tip944Test.java | 20 ++ .../core/vm/nativecontract/Tip944VmTest.java | 128 ++++++++++ .../TvmOptimizationVmTestBase.java | 225 ++++++++++++++++++ 3 files changed, 373 insertions(+) create mode 100644 framework/src/test/java/org/tron/core/vm/nativecontract/Tip944VmTest.java create mode 100644 framework/src/test/java/org/tron/core/vm/nativecontract/TvmOptimizationVmTestBase.java diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java index 2afbecb96ae..1b158ab859d 100644 --- a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java @@ -53,6 +53,7 @@ public static void init() { public void setup() { VMConfig.setGlobalSnapshot(new VMConfig.Snapshot()); ConfigLoader.disable = true; + VMConfig.initAllowTvmVote(1); VMConfig.initAllowTvmFreezeV2(1); VMConfig.initAllowTvmSelfdestructRestriction(1); VMConfig.initAllowTvmVotePowerOptimization(1); @@ -272,4 +273,23 @@ public void blackHoleReceiptAndTransferRollbackUseActualExecutionState() { assertEquals(0, legacy(OWNER)); assertEquals(0, repo.getAccount(BENEFICIARY).getFrozenV2BalanceForEnergy()); } + + @Test + public void voteCleanupPreservesInitializedAndInvalidStatesWithoutStakeTransfer() { + for (long prior : new long[]{3 * TRX_PRECISION, -1}) { + setup(); + AccountCapsule owner = repo.getAccount(OWNER); + owner.setOldTronPower(prior); + owner.addVotes(ByteString.copyFrom(WITNESS), 1); + owner.setAllowance(123); + repo.updateAccount(OWNER, owner); + program(repo, OWNER).suicide2(new DataWord(BENEFICIARY)); + assertEquals(prior, legacy(OWNER)); + assertTrue(repo.getAccount(OWNER).getVotesList().isEmpty()); + assertTrue(repo.getVotes(OWNER).getNewVotes().isEmpty()); + assertEquals(1, repo.getVotes(OWNER).getOldVotes().size()); + assertEquals(0, repo.getAccount(OWNER).getAllowance()); + assertEquals(123, repo.getAccount(BENEFICIARY).getBalance()); + } + } } diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944VmTest.java b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944VmTest.java new file mode 100644 index 00000000000..2370ab7d631 --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944VmTest.java @@ -0,0 +1,128 @@ +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.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.BANDWIDTH; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; +import org.tron.core.db.TransactionTrace; +import org.tron.core.store.StoreFactory; +import org.tron.core.vm.config.VMConfig; +import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.repository.RepositoryImpl; + +public class Tip944VmTest extends TvmOptimizationVmTestBase { + @Override + protected void enableOptimization() { + VMConfig.initAllowTvmVotePowerOptimization(1); + dbManager.getDynamicPropertiesStore().saveAllowTvmVotePowerOptimization(1); + } + + @Test + public void failedVoteRollsBackRewardAndInitialization() throws Exception { + install(SOURCE, vote(1, 2)); + stake(SOURCE, BANDWIDTH, TRX_PRECISION); + pendingReward(SOURCE); + byte[] before = account(SOURCE).getData(); + TransactionTrace failedVote = execute(SOURCE); + assertSuccess(failedVote); + assertEquals(0, returnWord(failedVote, 0)); + assertRejectedInternal(failedVote, "voteWitness"); + finish(failedVote); + assertArrayEquals(before, account(SOURCE).getData()); + assertEquals(1, dbManager.getDelegationStore().getBeginCycle(SOURCE)); + assertEquals(1, dbManager.getDelegationStore().getEndCycle(SOURCE)); + assertNull(dbManager.getDelegationStore().getAccountVote(2, SOURCE)); + + // Positive control: this same reward history really credits allowance on a valid vote. + Repository root = RepositoryImpl.createRoot(StoreFactory.getInstance()); + root.saveCode(SOURCE, Hex.decode(vote(1, 1))); + root.commit(); + TransactionTrace successfulVote = execute(SOURCE); + assertSuccess(successfulVote); + assertEquals(1, returnWord(successfulVote, 0)); + finish(successfulVote); + assertEquals(142, account(SOURCE).getAllowance()); + assertEquals(TRX_PRECISION, account(SOURCE).getInstance().getOldTronPower()); + assertEquals(2, dbManager.getDelegationStore().getBeginCycle(SOURCE)); + assertEquals(3, dbManager.getDelegationStore().getEndCycle(SOURCE)); + assertNotNull(dbManager.getDelegationStore().getAccountVote(2, SOURCE)); + } + + @Test + public void outerRevertRestoresSuccessfulChildVoteRewardAndInitialization() throws Exception { + install(SOURCE, vote(1, 1)); + install(OUTER, outer(SOURCE, true)); + stake(SOURCE, BANDWIDTH, TRX_PRECISION); + pendingReward(SOURCE); + byte[] before = account(SOURCE).getData(); + long[] beforeWeights = weights(); + TransactionTrace trace = execute(OUTER); + assertTrue(trace.getRuntime().getResult().isRevert()); + assertEquals(1, returnWord(trace, 0)); // Inner VOTEWITNESS succeeded before outer REVERT. + assertEquals(1, returnWord(trace, 1)); // The CALL also returned success. + assertRejectedInternal(trace, "voteWitness"); + finish(trace); + assertArrayEquals(before, account(SOURCE).getData()); + assertArrayEquals(beforeWeights, weights()); + assertEquals(1, dbManager.getDelegationStore().getBeginCycle(SOURCE)); + assertEquals(1, dbManager.getDelegationStore().getEndCycle(SOURCE)); + assertNull(dbManager.getDelegationStore().getAccountVote(2, SOURCE)); + assertNull(dbManager.getVotesStore().get(SOURCE)); + } + + @Test + public void outerRevertRestoresSuccessfulChildSelfDestructState() throws Exception { + VMConfig.initAllowTvmSelfdestructRestriction(0); + install(SOURCE, destroy(BENEFICIARY)); + install(OUTER, outer(SOURCE, true)); + stake(SOURCE, BANDWIDTH, 1_600_000); + stake(BENEFICIARY, BANDWIDTH, 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 scheduledSourceCannotReuseLegacyPowerBeforePhysicalDeletion() throws Exception { + VMConfig.initAllowTvmSelfdestructRestriction(0); + install(SOURCE, destroyOrVote(BENEFICIARY)); + install(OUTER, twoCalls(SOURCE, SOURCE, 32)); + stake(SOURCE, BANDWIDTH, 2 * TRX_PRECISION); + legacyVotes(SOURCE, 3 * TRX_PRECISION); + TransactionTrace trace = execute(OUTER); + assertSuccess(trace); + assertEquals(1, returnWord(trace, 1)); + assertEquals(1, returnWord(trace, 2)); + assertEquals(0, returnWord(trace, 0)); + assertEquals(1, trace.getRuntime().getResult().getInternalTransactions().stream() + .filter(tx -> "voteWitness".equals(tx.getNote()) && tx.isRejected()).count()); + assertNotNull(account(SOURCE)); + assertTrue(dbManager.getCodeStore().has(SOURCE)); + assertEquals(-1, account(SOURCE).getInstance().getOldTronPower()); + assertTrue(account(SOURCE).getVotesList().isEmpty()); + assertTrue(dbManager.getVotesStore().get(SOURCE).getNewVotes().isEmpty()); + assertFalse(trace.getRuntime().getResult().getDeleteAccounts().isEmpty()); + finish(trace); + assertNull(account(SOURCE)); + assertFalse(dbManager.getCodeStore().has(SOURCE)); + assertTrue(dbManager.getVotesStore().get(SOURCE).getNewVotes().isEmpty()); + } +} 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 new file mode 100644 index 00000000000..7fec55be146 --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/TvmOptimizationVmTestBase.java @@ -0,0 +1,225 @@ +package org.tron.core.vm.nativecontract; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.tron.core.config.Parameter.ChainConstant.TRX_PRECISION; + +import com.google.protobuf.ByteString; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Locale; +import org.bouncycastle.util.encoders.Hex; +import org.tron.common.BaseMethodTest; +import org.tron.common.runtime.RuntimeImpl; +import org.tron.common.runtime.TvmTestUtils; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.capsule.ContractCapsule; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.capsule.WitnessCapsule; +import org.tron.core.db.TransactionTrace; +import org.tron.core.store.DelegationStore; +import org.tron.core.store.StoreFactory; +import org.tron.core.vm.config.ConfigLoader; +import org.tron.core.vm.config.VMConfig; +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; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; + +/** Real transaction/CALL fixtures for the independent TVM optimization proposals. */ +public abstract class TvmOptimizationVmTestBase extends BaseMethodTest { + protected static final byte[] CALLER = address(21); + protected static final byte[] SOURCE = address(22); + protected static final byte[] BENEFICIARY = address(23); + protected static final byte[] OUTER = address(24); + protected static final byte[] WITNESS = address(25); + protected static final byte[] THIRD = address(26); + + @Override + protected String[] extraArgs() { + return new String[]{"--debug"}; + } + + @Override + protected void afterInit() { + ConfigLoader.disable = true; + VMConfig.setGlobalSnapshot(new VMConfig.Snapshot()); + VMConfig.initVmHardFork(true); + VMConfig.initAllowTvmTransferTrc10(1); + VMConfig.initAllowTvmConstantinople(1); + VMConfig.initAllowTvmSolidity059(1); + VMConfig.initAllowTvmIstanbul(1); + VMConfig.initAllowTvmFreeze(1); + VMConfig.initAllowTvmFreezeV2(1); + VMConfig.initAllowTvmVote(1); + VMConfig.initAllowEnergyAdjustment(1); + VMConfig.initAllowTvmSelfdestructRestriction(1); + dbManager.getDynamicPropertiesStore().saveAllowNewResourceModel(1); + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowTvmVote(1); + dbManager.getDynamicPropertiesStore().saveCurrentCycleNumber(0); + enableOptimization(); + Repository root = RepositoryImpl.createRoot(StoreFactory.getInstance()); + root.createAccount(CALLER, Protocol.AccountType.Normal); + root.addBalance(CALLER, 1_000_000_000_000_000L); + root.commit(); + dbManager.getWitnessStore().put(WITNESS, new WitnessCapsule( + Protocol.Witness.newBuilder().setAddress(ByteString.copyFrom(WITNESS)).build())); + install(BENEFICIARY, "00"); + } + + protected abstract void enableOptimization(); + + protected void install(byte[] address, String runtime) { + 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())); + root.saveCode(address, Hex.decode(runtime)); + root.commit(); + } + + protected AccountCapsule account(byte[] address) { + return dbManager.getAccountStore().get(address); + } + + protected void save(AccountCapsule account) { + dbManager.getAccountStore().put(account.createDbKey(), account); + } + + protected void stake(byte[] address, ResourceCode type, long amount) { + AccountCapsule account = account(address); + switch (type) { + case BANDWIDTH: + long oldNet = account.getFrozenV2BalanceWithDelegated(type) / TRX_PRECISION; + account.addFrozenBalanceForBandwidthV2(amount); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight( + dbManager.getDynamicPropertiesStore().getTotalNetWeight() + + account.getFrozenV2BalanceWithDelegated(type) / TRX_PRECISION - oldNet); + break; + case ENERGY: + long oldEnergy = account.getFrozenV2BalanceWithDelegated(type) / TRX_PRECISION; + account.addFrozenBalanceForEnergyV2(amount); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight( + dbManager.getDynamicPropertiesStore().getTotalEnergyWeight() + + account.getFrozenV2BalanceWithDelegated(type) / TRX_PRECISION - oldEnergy); + break; + default: + throw new IllegalArgumentException("Unsupported fixture stake type"); + } + save(account); + } + + protected long[] weights() { + return new long[]{dbManager.getDynamicPropertiesStore().getTotalNetWeight(), + dbManager.getDynamicPropertiesStore().getTotalEnergyWeight(), + dbManager.getDynamicPropertiesStore().getTotalTronPowerWeight()}; + } + + protected void legacyVotes(byte[] address, long legacy) { + AccountCapsule account = account(address); + account.setOldTronPower(legacy); + account.addVotes(ByteString.copyFrom(WITNESS), 1); + save(account); + } + + protected void pendingReward(byte[] address) { + legacyVotes(address, 0); + AccountCapsule account = account(address); + account.setAllowance(100); + save(account); + dbManager.getDynamicPropertiesStore().saveCurrentCycleNumber(2); + DelegationStore delegation = dbManager.getDelegationStore(); + delegation.setBeginCycle(address, 1); + delegation.setEndCycle(address, 1); + delegation.setWitnessVi(0, WITNESS, BigInteger.ZERO); + delegation.setWitnessVi(1, WITNESS, + DelegationStore.DECIMAL_OF_VI_REWARD.multiply(BigInteger.valueOf(42))); + } + + protected TransactionTrace execute(byte[] contract) throws Exception { + TransactionCapsule tx = new TransactionCapsule( + TvmTestUtils.generateTriggerSmartContractAndGetTransaction( + CALLER, contract, new byte[0], 0, 1_000_000_000L)); + TransactionTrace trace = + new TransactionTrace(tx, StoreFactory.getInstance(), new RuntimeImpl()); + tx.setTrxTrace(trace); + trace.init(null); + trace.exec(); + return trace; + } + + protected void finish(TransactionTrace trace) throws Exception { + trace.finalization(); + trace.setResult(); + } + + protected void assertSuccess(TransactionTrace trace) { + assertNull(trace.getRuntime().getRuntimeError()); + assertEquals(Protocol.Transaction.Result.contractResult.SUCCESS, + trace.getRuntime().getResult().getResultCode()); + } + + protected void assertRejectedInternal(TransactionTrace trace, String note) { + assertEquals(1, trace.getRuntime().getResult().getInternalTransactions().stream() + .filter(tx -> note.equals(tx.getNote()) && tx.isRejected()).count()); + } + + protected long returnWord(TransactionTrace trace, int index) { + byte[] data = trace.getRuntime().getResult().getHReturn(); + return new BigInteger(1, Arrays.copyOfRange(data, index * 32, (index + 1) * 32)).longValue(); + } + + protected static String push(int value) { + return "61" + String.format(Locale.ROOT, "%04x", value); + } + + protected static String pushAddress(byte[] address) { + return "73" + Hex.toHexString(Arrays.copyOfRange(address, 1, 21)); + } + + protected static String destroy(byte[] beneficiary) { + return pushAddress(beneficiary) + "ff"; + } + + protected static String vote(int count, int amount) { + // Two ABI-style arrays at memory offsets 0 and 2048; VOTEWITNESS returns one stack word. + String headers = push(count) + push(0) + "52" + push(count) + push(2048) + "52"; + String entry = pushAddress(WITNESS) + push(32) + "52" + + push(amount) + push(2080) + "52"; + return headers + entry + push(0) + push(count) + push(2048) + push(count) + "d8" + + push(0) + "52" + push(32) + push(0) + "f3"; + } + + protected static String call(byte[] target, int inputSize) { + return push(32) + push(0) + push(inputSize) + push(0) + push(0) + + pushAddress(target) + "5af1"; + } + + protected static String outer(byte[] child, boolean revert) { + // Return/revert the inner return value and CALL success, proving successful child execution. + return call(child, 0) + push(32) + "52" + push(64) + push(0) + (revert ? "fd" : "f3"); + } + + protected static String twoCalls(byte[] first, byte[] second, int secondInputSize) { + return call(first, 0) + push(32) + "52" + call(second, secondInputSize) + + push(64) + "52" + push(96) + push(0) + "f3"; + } + + protected static String destroyOrVote(byte[] beneficiary) { + String destruction = destroy(beneficiary); + int voteOffset = 5 + Hex.decode(destruction).length; + return "3661" + String.format(Locale.ROOT, "%04x", voteOffset) + "57" + + destruction + "5b" + vote(1, 1); + } + + private static byte[] address(int last) { + byte[] address = new byte[21]; + address[0] = 0x41; + address[20] = (byte) last; + return address; + } +} From c5f0c8ba4a23cefa3022036a798eac6aaed1fcf2 Mon Sep 17 00:00:00 2001 From: Asuka Date: Tue, 27 Oct 2026 18:07:10 +0800 Subject: [PATCH 3/4] fix(vm): align Stake 1.0 voting power transitions --- .../FreezeBalanceProcessor.java | 6 + .../UnfreezeBalanceProcessor.java | 24 +++ .../core/vm/nativecontract/Tip944Test.java | 194 ++++++++++++++++++ .../core/vm/nativecontract/Tip944VmTest.java | 146 +++++++++++++ 4 files changed, 370 insertions(+) diff --git a/actuator/src/main/java/org/tron/core/vm/nativecontract/FreezeBalanceProcessor.java b/actuator/src/main/java/org/tron/core/vm/nativecontract/FreezeBalanceProcessor.java index 3088527ace6..21821ebd69a 100644 --- a/actuator/src/main/java/org/tron/core/vm/nativecontract/FreezeBalanceProcessor.java +++ b/actuator/src/main/java/org/tron/core/vm/nativecontract/FreezeBalanceProcessor.java @@ -11,6 +11,7 @@ import org.tron.core.capsule.DelegatedResourceCapsule; import org.tron.core.exception.ContractValidateException; import org.tron.core.store.DynamicPropertiesStore; +import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.nativecontract.param.FreezeBalanceParam; import org.tron.core.vm.repository.Repository; import org.tron.protos.Protocol; @@ -80,6 +81,11 @@ public void execute(FreezeBalanceParam param, Repository repo) { byte[] receiverAddress = param.getReceiverAddress(); long frozenBalance = param.getFrozenBalance(); AccountCapsule accountCapsule = repo.getAccount(ownerAddress); + if (VMConfig.allowTvmVotePowerOptimization() + && dynamicStore.supportAllowNewResourceModel() + && accountCapsule.oldTronPowerIsNotInitialized()) { + accountCapsule.initializeOldTronPower(); + } // acquire or delegate resource if (param.isDelegating()) { // delegate resource switch (param.getResourceType()) { diff --git a/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceProcessor.java b/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceProcessor.java index 53981a22d34..8afe5d36dfb 100644 --- a/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceProcessor.java +++ b/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceProcessor.java @@ -105,7 +105,15 @@ public long execute(UnfreezeBalanceParam param, Repository repo) { byte[] ownerAddress = param.getOwnerAddress(); byte[] receiverAddress = param.getReceiverAddress(); + boolean optimizeVotePower = VMConfig.allowTvmVotePowerOptimization() + && repo.getDynamicPropertiesStore().supportAllowNewResourceModel(); + if (optimizeVotePower) { + VoteRewardUtil.withdrawReward(ownerAddress, repo); + } AccountCapsule accountCapsule = repo.getAccount(ownerAddress); + if (optimizeVotePower && accountCapsule.oldTronPowerIsNotInitialized()) { + accountCapsule.initializeOldTronPower(); + } long oldBalance = accountCapsule.getBalance(); long unfreezeBalance = 0L; @@ -202,6 +210,22 @@ public long execute(UnfreezeBalanceParam param, Repository repo) { repo.updateAccount(accountCapsule.createDbKey(), accountCapsule); + if (optimizeVotePower) { + if (!accountCapsule.oldTronPowerIsInvalid()) { + VotesCapsule votesCapsule = repo.getVotes(ownerAddress); + if (votesCapsule == null) { + votesCapsule = new VotesCapsule(ByteString.copyFrom(ownerAddress), + accountCapsule.getVotesList()); + } + votesCapsule.clearNewVotes(); + accountCapsule.clearVotes(); + accountCapsule.invalidateOldTronPower(); + repo.updateVotes(ownerAddress, votesCapsule); + repo.updateAccount(ownerAddress, accountCapsule); + } + return unfreezeBalance; + } + if (VMConfig.allowTvmVote() && !accountCapsule.getVotesList().isEmpty()) { long usedTronPower = 0; for (Protocol.Vote vote : accountCapsule.getVotesList()) { diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java index 1b158ab859d..a9e5aaea251 100644 --- a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java @@ -1,5 +1,6 @@ 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.assertThrows; @@ -12,6 +13,7 @@ import static org.tron.protos.contract.Common.ResourceCode.TRON_POWER; import com.google.protobuf.ByteString; +import java.util.Collections; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -22,6 +24,7 @@ import org.tron.common.utils.ForkController; import org.tron.core.capsule.AccountCapsule; import org.tron.core.capsule.ProposalCapsule; +import org.tron.core.capsule.VotesCapsule; import org.tron.core.capsule.WitnessCapsule; import org.tron.core.config.Parameter.ForkBlockVersionEnum; import org.tron.core.config.args.Args; @@ -31,6 +34,8 @@ 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.nativecontract.param.FreezeBalanceParam; +import org.tron.core.vm.nativecontract.param.UnfreezeBalanceParam; import org.tron.core.vm.program.Program; import org.tron.core.vm.program.invoke.ProgramInvoke; import org.tron.core.vm.repository.Repository; @@ -59,6 +64,7 @@ public void setup() { VMConfig.initAllowTvmVotePowerOptimization(1); dbManager.getDynamicPropertiesStore().saveAllowNewResourceModel(1); dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveCurrentCycleNumber(0); dbManager.getDynamicPropertiesStore().saveAllowTvmVotePowerOptimization(0); repo = RepositoryImpl.createRoot(StoreFactory.getInstance()); repo.createAccount(OWNER, Protocol.AccountType.Contract); @@ -292,4 +298,192 @@ public void voteCleanupPreservesInitializedAndInvalidStatesWithoutStakeTransfer( assertEquals(123, repo.getAccount(BENEFICIARY).getBalance()); } } + + private byte[] stakeOneReceiver(boolean delegated) { + if (delegated) { + repo.createAccount(BENEFICIARY, Protocol.AccountType.Normal); + } + return delegated ? BENEFICIARY : OWNER; + } + + private void freezeOne(Repository repository, byte[] receiver, ResourceCode type, long amount) + throws Exception { + FreezeBalanceParam param = new FreezeBalanceParam(); + param.setOwnerAddress(OWNER); + param.setReceiverAddress(receiver); + param.setResourceType(type); + param.setFrozenBalance(amount); + param.setFrozenDuration(0); + FreezeBalanceProcessor processor = new FreezeBalanceProcessor(); + processor.validate(param, repository); + processor.execute(param, repository); + } + + private void unfreezeOne(Repository repository, byte[] receiver, ResourceCode type) + throws Exception { + UnfreezeBalanceParam param = new UnfreezeBalanceParam(); + param.setOwnerAddress(OWNER); + param.setReceiverAddress(receiver); + param.setResourceType(type); + UnfreezeBalanceProcessor processor = new UnfreezeBalanceProcessor(); + processor.validate(param, repository); + assertEquals(5 * TRX_PRECISION, processor.execute(param, repository)); + } + + private void fundOwner() { + AccountCapsule owner = repo.getAccount(OWNER); + owner.setBalance(5 * TRX_PRECISION); + repo.updateAccount(OWNER, owner); + } + + @Test + public void stakeOneFreezeInitializesBeforeOwnOrDelegatedStakeAndPreservesExistingStates() + throws Exception { + for (ResourceCode type : new ResourceCode[]{BANDWIDTH, ENERGY}) { + for (boolean delegated : new boolean[]{false, true}) { + for (long prior : new long[]{0, 7 * TRX_PRECISION, -1}) { + setup(); + VMConfig.initAllowTvmFreezeV2(0); + byte[] receiver = stakeOneReceiver(delegated); + fundOwner(); + stake(OWNER, ENERGY, 2 * TRX_PRECISION); + legacy(OWNER, prior); + freezeOne(repo, receiver, type, 5 * TRX_PRECISION); + assertEquals(prior == 0 ? 2 * TRX_PRECISION : prior, legacy(OWNER)); + assertEquals(7 * TRX_PRECISION, repo.getAccount(OWNER).getTronPower()); + assertEquals(0, legacy(BENEFICIARY)); + if (delegated) { + assertEquals(0, repo.getAccount(BENEFICIARY).getTronPower()); + } + } + } + } + } + + @Test + public void stakeOneLifecycleIsGatedAndDoesNotDependOnUnfreezeDelay() throws Exception { + for (long enabled : new long[]{0, 1}) { + for (long newModel : new long[]{0, 1}) { + for (long delay : new long[]{0, 14}) { + setup(); + VMConfig.initAllowTvmFreezeV2(0); + VMConfig.initAllowTvmVotePowerOptimization(enabled); + dbManager.getDynamicPropertiesStore().saveAllowNewResourceModel(newModel); + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(delay); + fundOwner(); + freezeOne(repo, OWNER, BANDWIDTH, 5 * TRX_PRECISION); + boolean optimized = enabled == 1 && newModel == 1; + assertEquals(optimized ? -1 : 0, legacy(OWNER)); + legacy(OWNER, 5 * TRX_PRECISION); + unfreezeOne(repo, OWNER, BANDWIDTH); + assertEquals(optimized ? -1 : 5 * TRX_PRECISION, legacy(OWNER)); + assertEquals(!optimized && newModel == 1 && delay > 0, vote(repo, OWNER, 5)); + } + } + } + } + + @Test + public void stakeOneUnfreezeInvalidatesEntireSnapshotEvenWithRemainingResourcePower() + throws Exception { + for (ResourceCode type : new ResourceCode[]{BANDWIDTH, ENERGY}) { + for (boolean delegated : new boolean[]{false, true}) { + for (long prior : new long[]{0, 7 * TRX_PRECISION, -1}) { + setup(); + byte[] receiver = stakeOneReceiver(delegated); + fundOwner(); + VMConfig.initAllowTvmVotePowerOptimization(0); + freezeOne(repo, receiver, type, 5 * TRX_PRECISION); + VMConfig.initAllowTvmVotePowerOptimization(1); + stake(OWNER, ENERGY, 10 * TRX_PRECISION); + stake(OWNER, TRON_POWER, 2 * TRX_PRECISION); + AccountCapsule owner = repo.getAccount(OWNER); + owner.setOldTronPower(prior); + owner.addVotes(ByteString.copyFrom(WITNESS), 1); + repo.updateAccount(OWNER, owner); + VotesCapsule votes = new VotesCapsule(ByteString.copyFrom(OWNER), owner.getVotesList()); + votes.addAllNewVotes(owner.getVotesList()); + repo.updateVotes(OWNER, votes); + unfreezeOne(repo, receiver, type); + assertEquals(-1, legacy(OWNER)); + assertEquals(2 * TRX_PRECISION, repo.getAccount(OWNER).getAllTronPower()); + assertEquals(prior == -1 ? 1 : 0, repo.getAccount(OWNER).getVotesList().size()); + assertEquals(prior == -1 ? 1 : 0, repo.getVotes(OWNER).getNewVotes().size()); + assertEquals(1, repo.getVotes(OWNER).getOldVotes().size()); + assertTrue(vote(repo, OWNER, 2)); + assertFalse(vote(repo, OWNER, 3)); + } + } + } + } + + @Test + public void stakeOneUnfreezePreservesExplicitPowerVotesAfterInvalidation() throws Exception { + for (ResourceCode type : new ResourceCode[]{BANDWIDTH, ENERGY}) { + setup(); + fundOwner(); + freezeOne(repo, OWNER, type, 5 * TRX_PRECISION); + stake(OWNER, TRON_POWER, 2 * TRX_PRECISION); + assertTrue(vote(repo, OWNER, 2)); + byte[] votesBefore = repo.getVotes(OWNER).getData(); + unfreezeOne(repo, OWNER, type); + assertEquals(0, repo.getAccount(OWNER).getTronPower()); + assertEquals(-1, legacy(OWNER)); + assertEquals(2, repo.getAccount(OWNER).getVotesList().get(0).getVoteCount()); + assertArrayEquals(votesBefore, repo.getVotes(OWNER).getData()); + } + } + + @Test + public void stakeOneVoteRetentionIsGatedByOptimizationAndNewResourceModel() throws Exception { + for (long enabled : new long[]{0, 1}) { + for (long newModel : new long[]{0, 1}) { + setup(); + fundOwner(); + freezeOne(repo, OWNER, ENERGY, 5 * TRX_PRECISION); + stake(OWNER, TRON_POWER, 2 * TRX_PRECISION); + assertTrue(vote(repo, OWNER, 2)); + VMConfig.initAllowTvmVotePowerOptimization(enabled); + dbManager.getDynamicPropertiesStore().saveAllowNewResourceModel(newModel); + unfreezeOne(repo, OWNER, ENERGY); + assertEquals(-1, legacy(OWNER)); + assertEquals(enabled == 1 && newModel == 1 ? 1 : 0, + repo.getAccount(OWNER).getVotesList().size()); + } + } + } + + @Test + public void stakeOneUnfreezeInvalidatesWithoutVotesAndClearsPendingVotes() throws Exception { + fundOwner(); + VMConfig.initAllowTvmVotePowerOptimization(0); + freezeOne(repo, OWNER, BANDWIDTH, 5 * TRX_PRECISION); + VMConfig.initAllowTvmVotePowerOptimization(1); + VotesCapsule votes = new VotesCapsule(ByteString.copyFrom(OWNER), + Collections.emptyList()); + votes.addAllNewVotes(Collections.singletonList(Protocol.Vote.newBuilder() + .setVoteAddress(ByteString.copyFrom(WITNESS)).setVoteCount(1).build())); + repo.updateVotes(OWNER, votes); + unfreezeOne(repo, OWNER, BANDWIDTH); + assertEquals(-1, legacy(OWNER)); + assertTrue(repo.getVotes(OWNER).getNewVotes().isEmpty()); + } + + @Test + public void invalidStakeOneOperationsDoNotInitializeLegacyPower() { + VMConfig.initAllowTvmFreeze(1); + VMConfig.initAllowTvmFreezeV2(0); + Program p = program(repo, OWNER); + assertFalse(p.freeze(new DataWord(OWNER), new DataWord(5 * TRX_PRECISION), DataWord.ZERO())); + assertEquals(0, legacy(OWNER)); + assertFalse(p.unfreeze(new DataWord(OWNER), DataWord.ZERO())); + assertEquals(0, legacy(OWNER)); + AccountCapsule owner = repo.getAccount(OWNER); + owner.setFrozenForBandwidth(5 * TRX_PRECISION, + dbManager.getDynamicPropertiesStore().getLatestBlockHeaderTimestamp() + 1); + repo.updateAccount(OWNER, owner); + assertFalse(p.unfreeze(new DataWord(OWNER), DataWord.ZERO())); + assertEquals(0, legacy(OWNER)); + assertEquals(5 * TRX_PRECISION, repo.getAccount(OWNER).getFrozenBalance()); + } } diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944VmTest.java b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944VmTest.java index 2370ab7d631..db8bbf035c7 100644 --- a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944VmTest.java +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944VmTest.java @@ -11,6 +11,7 @@ import org.bouncycastle.util.encoders.Hex; import org.junit.Test; +import org.tron.core.capsule.AccountCapsule; import org.tron.core.db.TransactionTrace; import org.tron.core.store.StoreFactory; import org.tron.core.vm.config.VMConfig; @@ -125,4 +126,149 @@ public void scheduledSourceCannotReuseLegacyPowerBeforePhysicalDeletion() throws assertFalse(dbManager.getCodeStore().has(SOURCE)); assertTrue(dbManager.getVotesStore().get(SOURCE).getNewVotes().isEmpty()); } + + private void replaceCode(byte[] address, String code) { + Repository root = RepositoryImpl.createRoot(StoreFactory.getInstance()); + root.saveCode(address, Hex.decode(code)); + root.commit(); + } + + private static String stakeOneCode(boolean freeze) { + return pushAddress(SOURCE) + (freeze ? "63004c4b40" : "") + push(0) + + (freeze ? "d5" : "d6") + push(0) + "52" + push(32) + push(0) + "f3"; + } + + @Test + public void stakeOneOpcodesUseSnapshotRulesWithIndependentVmFlags() throws Exception { + // Override the VM flags independently to exercise the getAllTronPower path as well. + VMConfig.initAllowTvmFreezeV2(0); + install(SOURCE, stakeOneCode(true)); + AccountCapsule source = account(SOURCE); + source.setBalance(5 * TRX_PRECISION); + save(source); + TransactionTrace frozen = execute(SOURCE); + assertSuccess(frozen); + assertEquals(1, returnWord(frozen, 0)); + finish(frozen); + assertEquals(5 * TRX_PRECISION, account(SOURCE).getFrozenBalance()); + assertEquals(-1, account(SOURCE).getInstance().getOldTronPower()); + replaceCode(SOURCE, vote(1, 5)); + TransactionTrace rejectedVote = execute(SOURCE); + assertSuccess(rejectedVote); + assertEquals(0, returnWord(rejectedVote, 0)); + assertRejectedInternal(rejectedVote, "voteWitness"); + finish(rejectedVote); + + dbManager.getDynamicPropertiesStore().saveLatestBlockHeaderTimestamp( + account(SOURCE).getFrozenList().get(0).getExpireTime() + 1); + replaceCode(SOURCE, stakeOneCode(false)); + TransactionTrace unfrozen = execute(SOURCE); + assertSuccess(unfrozen); + assertEquals(1, returnWord(unfrozen, 0)); + finish(unfrozen); + assertEquals(0, account(SOURCE).getFrozenBalance()); + assertEquals(-1, account(SOURCE).getInstance().getOldTronPower()); + replaceCode(SOURCE, vote(1, 5)); + TransactionTrace afterUnfreeze = execute(SOURCE); + assertSuccess(afterUnfreeze); + assertEquals(0, returnWord(afterUnfreeze, 0)); + finish(afterUnfreeze); + } + + @Test + public void stakeOneOnlyConfigurationPreservesVotingPowerSelectionWithoutUnfreezeDelay() + throws Exception { + VMConfig.initAllowTvmFreezeV2(0); + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); + install(SOURCE, stakeOneCode(true)); + AccountCapsule source = account(SOURCE); + source.setBalance(5 * TRX_PRECISION); + save(source); + TransactionTrace frozen = execute(SOURCE); + assertSuccess(frozen); + assertEquals(1, returnWord(frozen, 0)); + finish(frozen); + assertEquals(-1, account(SOURCE).getInstance().getOldTronPower()); + + replaceCode(SOURCE, vote(1, 5)); + TransactionTrace voted = execute(SOURCE); + assertSuccess(voted); + assertEquals(1, returnWord(voted, 0)); // Existing selection still uses getTronPower(). + finish(voted); + assertEquals(-1, account(SOURCE).getInstance().getOldTronPower()); + + dbManager.getDynamicPropertiesStore().saveLatestBlockHeaderTimestamp( + account(SOURCE).getFrozenList().get(0).getExpireTime() + 1); + replaceCode(SOURCE, stakeOneCode(false)); + TransactionTrace unfrozen = execute(SOURCE); + assertSuccess(unfrozen); + assertEquals(1, returnWord(unfrozen, 0)); + finish(unfrozen); + assertEquals(-1, account(SOURCE).getInstance().getOldTronPower()); + replaceCode(SOURCE, vote(1, 5)); + TransactionTrace rejectedVote = execute(SOURCE); + assertSuccess(rejectedVote); + assertEquals(0, returnWord(rejectedVote, 0)); + finish(rejectedVote); + } + + @Test + public void outerRevertRestoresStakeOneFreezeAndUninitializedSnapshot() throws Exception { + VMConfig.initAllowTvmFreezeV2(0); + install(SOURCE, stakeOneCode(true)); + install(OUTER, outer(SOURCE, true)); + AccountCapsule source = account(SOURCE); + source.setBalance(5 * TRX_PRECISION); + save(source); + byte[] before = account(SOURCE).getData(); + long[] beforeWeights = weights(); + TransactionTrace trace = execute(OUTER); + assertTrue(trace.getRuntime().getResult().isRevert()); + assertEquals(1, returnWord(trace, 0)); + assertEquals(1, returnWord(trace, 1)); + assertRejectedInternal(trace, "freezeForBandwidth"); + finish(trace); + assertArrayEquals(before, account(SOURCE).getData()); + assertArrayEquals(beforeWeights, weights()); + } + + @Test + public void outerRevertRestoresStakeOneUnfreezeSnapshotVotesAndReward() throws Exception { + VMConfig.initAllowTvmFreezeV2(0); + install(SOURCE, stakeOneCode(false)); + install(OUTER, outer(SOURCE, true)); + AccountCapsule source = account(SOURCE); + source.setFrozenForBandwidth(5 * TRX_PRECISION, 0); + save(source); + pendingReward(SOURCE); + byte[] before = account(SOURCE).getData(); + long[] beforeWeights = weights(); + TransactionTrace trace = execute(OUTER); + assertTrue(trace.getRuntime().getResult().isRevert()); + assertEquals(1, returnWord(trace, 0)); + assertEquals(1, returnWord(trace, 1)); + assertRejectedInternal(trace, "unfreezeForBandwidth"); + finish(trace); + assertArrayEquals(before, account(SOURCE).getData()); + assertArrayEquals(beforeWeights, weights()); + assertNull(dbManager.getVotesStore().get(SOURCE)); + assertEquals(1, dbManager.getDelegationStore().getBeginCycle(SOURCE)); + assertEquals(1, dbManager.getDelegationStore().getEndCycle(SOURCE)); + assertNull(dbManager.getDelegationStore().getAccountVote(2, SOURCE)); + } + + @Test + public void stakeTwoActivationKeepsStakeOneFreezeOpcodeDisabled() throws Exception { + install(SOURCE, stakeOneCode(true)); + AccountCapsule source = account(SOURCE); + source.setBalance(5 * TRX_PRECISION); + save(source); + byte[] before = account(SOURCE).getData(); + TransactionTrace trace = execute(SOURCE); + assertSuccess(trace); + assertEquals(0, returnWord(trace, 0)); + finish(trace); + assertArrayEquals(before, account(SOURCE).getData()); + assertTrue(trace.getRuntime().getResult().getInternalTransactions().isEmpty()); + } } From de578c19c9ae1c496360a65c5915db4f267de980 Mon Sep 17 00:00:00 2001 From: Asuka Date: Thu, 29 Oct 2026 17:58:43 +0800 Subject: [PATCH 4/4] fix(vm): retain vote cleanup when unfreeze delay is disabled --- .../UnfreezeBalanceProcessor.java | 7 +++- .../core/vm/nativecontract/Tip944Test.java | 38 +++++++++++++++++++ .../core/vm/nativecontract/Tip944VmTest.java | 2 + 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceProcessor.java b/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceProcessor.java index 8afe5d36dfb..a7bfa5b8795 100644 --- a/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceProcessor.java +++ b/actuator/src/main/java/org/tron/core/vm/nativecontract/UnfreezeBalanceProcessor.java @@ -210,7 +210,7 @@ public long execute(UnfreezeBalanceParam param, Repository repo) { repo.updateAccount(accountCapsule.createDbKey(), accountCapsule); - if (optimizeVotePower) { + if (optimizeVotePower && repo.getDynamicPropertiesStore().supportUnfreezeDelay()) { if (!accountCapsule.oldTronPowerIsInvalid()) { VotesCapsule votesCapsule = repo.getVotes(ownerAddress); if (votesCapsule == null) { @@ -247,6 +247,11 @@ public long execute(UnfreezeBalanceParam param, Repository repo) { } } + if (optimizeVotePower && !accountCapsule.oldTronPowerIsInvalid()) { + accountCapsule.invalidateOldTronPower(); + repo.updateAccount(ownerAddress, accountCapsule); + } + return unfreezeBalance; } } diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java index a9e5aaea251..f5ebc058956 100644 --- a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944Test.java @@ -469,6 +469,44 @@ public void stakeOneUnfreezeInvalidatesWithoutVotesAndClearsPendingVotes() throw assertTrue(repo.getVotes(OWNER).getNewVotes().isEmpty()); } + @Test + public void stakeOneUnfreezeWithoutDelayUsesRemainingResourcePowerForVoteCleanup() + throws Exception { + for (ResourceCode type : new ResourceCode[]{BANDWIDTH, ENERGY}) { + for (boolean delegated : new boolean[]{false, true}) { + for (long prior : new long[]{0, 7 * TRX_PRECISION, -1}) { + for (long voteCount : new long[]{1, 2, 3}) { + setup(); + VMConfig.initAllowTvmFreezeV2(0); + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); + byte[] receiver = stakeOneReceiver(delegated); + fundOwner(); + freezeOne(repo, receiver, type, 5 * TRX_PRECISION); + stake(OWNER, TRON_POWER, 5 * TRX_PRECISION); + AccountCapsule owner = repo.getAccount(OWNER); + if (type == BANDWIDTH) { + owner.setFrozenForEnergy(2 * TRX_PRECISION, 0); + } else { + owner.setFrozenForBandwidth(2 * TRX_PRECISION, 0); + } + owner.setOldTronPower(prior); + owner.addVotes(ByteString.copyFrom(WITNESS), voteCount); + repo.updateAccount(OWNER, owner); + VotesCapsule votes = new VotesCapsule(ByteString.copyFrom(OWNER), owner.getVotesList()); + votes.addAllNewVotes(owner.getVotesList()); + repo.updateVotes(OWNER, votes); + unfreezeOne(repo, receiver, type); + assertEquals(-1, legacy(OWNER)); + assertEquals(2 * TRX_PRECISION, repo.getAccount(OWNER).getTronPower()); + assertEquals(voteCount > 2 ? 0 : 1, repo.getAccount(OWNER).getVotesList().size()); + assertEquals(voteCount > 2 ? 0 : 1, repo.getVotes(OWNER).getNewVotes().size()); + assertEquals(votes.getOldVotes(), repo.getVotes(OWNER).getOldVotes()); + } + } + } + } + } + @Test public void invalidStakeOneOperationsDoNotInitializeLegacyPower() { VMConfig.initAllowTvmFreeze(1); diff --git a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944VmTest.java b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944VmTest.java index db8bbf035c7..60f053e883f 100644 --- a/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944VmTest.java +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip944VmTest.java @@ -205,6 +205,8 @@ public void stakeOneOnlyConfigurationPreservesVotingPowerSelectionWithoutUnfreez assertEquals(1, returnWord(unfrozen, 0)); finish(unfrozen); assertEquals(-1, account(SOURCE).getInstance().getOldTronPower()); + assertTrue(account(SOURCE).getVotesList().isEmpty()); + assertTrue(dbManager.getVotesStore().get(SOURCE).getNewVotes().isEmpty()); replaceCode(SOURCE, vote(1, 5)); TransactionTrace rejectedVote = execute(SOURCE); assertSuccess(rejectedVote);