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..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 @@ -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); @@ -948,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; @@ -1182,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 @@ -1775,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); @@ -2321,6 +2366,16 @@ 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; + } + + if (VMConfig.allowTvmResourceOptimization() && witnessArrayLength > MAX_VOTE_NUMBER) { + if (internalTx != null) { + internalTx.reject(); + } return false; } 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/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()); } 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/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); + } + } +} 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/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, 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..ca69ca7a361 --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945Test.java @@ -0,0 +1,376 @@ +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.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; +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() { + 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(resourceOptimization); + 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)); + 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 + 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() { + setup(0); + stake(OWNER, BANDWIDTH, 1_600_000); + stake(BENEFICIARY, BANDWIDTH, 2_600_000); + 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()); + } + + @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); + 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(resource); + VMConfig.initAllowTvmVotePowerOptimization(power); + 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..ddf31777183 --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/nativecontract/Tip945VmTest.java @@ -0,0 +1,185 @@ +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]); + } + + @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(); } 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; + } }