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 cd42d7a9010..6b297883fbd 100644 --- a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java @@ -886,6 +886,22 @@ public static void validator(DynamicPropertiesStore dynamicPropertiesStore, } break; } + case ALLOW_HARDEN_RESOURCE_CALCULATION: { + if (!forkController.pass(ForkBlockVersionEnum.VERSION_4_8_2)) { + throw new ContractValidateException( + "Bad chain parameter id [ALLOW_HARDEN_RESOURCE_CALCULATION]"); + } + if (dynamicPropertiesStore.getAllowHardenResourceCalculation() == 1) { + throw new ContractValidateException( + "[ALLOW_HARDEN_RESOURCE_CALCULATION] has been valid, " + + "no need to propose again"); + } + if (value != 1) { + throw new ContractValidateException( + "This value[ALLOW_HARDEN_RESOURCE_CALCULATION] is only allowed to be 1"); + } + break; + } default: break; } @@ -971,7 +987,8 @@ public enum ProposalType { // current value, value range ALLOW_TVM_BLOB(89), // 0, 1 PROPOSAL_EXPIRE_TIME(92), // (0, 31536003000) ALLOW_TVM_SELFDESTRUCT_RESTRICTION(94), // 0, 1 - ALLOW_TVM_OSAKA(96); // 0, 1 + ALLOW_TVM_OSAKA(96), // 0, 1 + ALLOW_HARDEN_RESOURCE_CALCULATION(97); // 0, 1 private 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 e099101912b..881eb861bea 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 @@ -46,6 +46,7 @@ public static void load(StoreFactory storeFactory) { VMConfig.initAllowTvmBlob(ds.getAllowTvmBlob()); VMConfig.initAllowTvmSelfdestructRestriction(ds.getAllowTvmSelfdestructRestriction()); VMConfig.initAllowTvmOsaka(ds.getAllowTvmOsaka()); + VMConfig.initAllowHardenResourceCalculation(ds.getAllowHardenResourceCalculation()); } } } 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 9de7c0691ba..22d72246c06 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 @@ -8,6 +8,7 @@ import com.google.common.collect.HashBasedTable; import com.google.protobuf.ByteString; +import java.math.BigInteger; import java.util.HashMap; import java.util.HashSet; import java.util.Optional; @@ -17,6 +18,7 @@ import org.bouncycastle.util.Strings; import org.bouncycastle.util.encoders.Hex; import org.tron.common.crypto.Hash; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.vm.DataWord; import org.tron.common.utils.ByteArray; @@ -223,7 +225,7 @@ public Pair getAccountEnergyUsageBalanceAndRestoreSeconds(AccountCap long totalEnergyLimit = getDynamicPropertiesStore().getTotalEnergyCurrentLimit(); long totalEnergyWeight = getTotalEnergyWeight(); - long balance = (long) ((double) newEnergyUsage * totalEnergyWeight / totalEnergyLimit * TRX_PRECISION); + long balance = usageToBalance(newEnergyUsage, totalEnergyWeight, totalEnergyLimit); return Pair.of(balance, restoreSlots * BLOCK_PRODUCED_INTERVAL / 1_000); } @@ -246,11 +248,22 @@ public Pair getAccountNetUsageBalanceAndRestoreSeconds(AccountCapsul long totalNetLimit = getDynamicPropertiesStore().getTotalNetLimit(); long totalNetWeight = getTotalNetWeight(); - long balance = (long) ((double) newNetUsage * totalNetWeight / totalNetLimit * TRX_PRECISION); + long balance = usageToBalance(newNetUsage, totalNetWeight, totalNetLimit); return Pair.of(balance, restoreSlots * BLOCK_PRODUCED_INTERVAL / 1_000); } + private long usageToBalance(long usage, long totalWeight, long totalLimit) { + if (hardenResourceCalculation()) { + return BigInteger.valueOf(usage) + .multiply(BigInteger.valueOf(totalWeight)) + .multiply(BigInteger.valueOf(TRX_PRECISION)) + .divide(BigInteger.valueOf(totalLimit)) + .longValueExact(); + } + return (long) ((double) usage * totalWeight / totalLimit * TRX_PRECISION); + } + @Override public AssetIssueCapsule getAssetIssue(byte[] tokenId) { byte[] tokenIdWithoutLeadingZero = ByteUtil.stripLeadingZeroes(tokenId); @@ -896,8 +909,19 @@ private long recover(long lastUsage, long lastTime, long now, long personalWindo } private long increase(long lastUsage, long usage, long lastTime, long now, long windowSize) { - long averageLastUsage = divideCeil(lastUsage * precision, windowSize); - long averageUsage = divideCeil(usage * precision, windowSize); + long averageLastUsage; + long averageUsage; + if (hardenResourceCalculation()) { + BigInteger biPrecision = BigInteger.valueOf(precision); + BigInteger biWindowSize = BigInteger.valueOf(windowSize); + averageLastUsage = divideCeilExact( + BigInteger.valueOf(lastUsage).multiply(biPrecision), biWindowSize); + averageUsage = divideCeilExact( + BigInteger.valueOf(usage).multiply(biPrecision), biWindowSize); + } else { + averageLastUsage = divideCeil(lastUsage * precision, windowSize); + averageUsage = divideCeil(usage * precision, windowSize); + } if (lastTime != now) { assert now > lastTime; @@ -917,21 +941,46 @@ private long divideCeil(long numerator, long denominator) { return (numerator / denominator) + ((numerator % denominator) > 0 ? 1 : 0); } + private long divideCeilExact(BigInteger numerator, BigInteger denominator) { + BigInteger[] divRem = numerator.divideAndRemainder(denominator); + long result = divRem[0].longValueExact(); + if (divRem[1].signum() > 0) { + result = StrictMathWrapper.addExact(result, 1); + } + return result; + } + private long getUsage(long usage, long windowSize) { + if (hardenResourceCalculation()) { + return BigInteger.valueOf(usage) + .multiply(BigInteger.valueOf(windowSize)) + .divide(BigInteger.valueOf(precision)) + .longValueExact(); + } return usage * windowSize / precision; } + private boolean hardenResourceCalculation() { + return VMConfig.allowHardenResourceCalculation(); + } + public long calculateGlobalEnergyLimit(AccountCapsule accountCapsule) { long frozeBalance = accountCapsule.getAllFrozenBalanceForEnergy(); - if (frozeBalance < 1_000_000L) { + if (frozeBalance < precision) { return 0; } - long energyWeight = frozeBalance / 1_000_000L; + long energyWeight = frozeBalance / precision; long totalEnergyLimit = getDynamicPropertiesStore().getTotalEnergyCurrentLimit(); long totalEnergyWeight = getDynamicPropertiesStore().getTotalEnergyWeight(); assert totalEnergyWeight > 0; + if (hardenResourceCalculation()) { + return BigInteger.valueOf(energyWeight) + .multiply(BigInteger.valueOf(totalEnergyLimit)) + .divide(BigInteger.valueOf(totalEnergyWeight)) + .longValueExact(); + } return (long) (energyWeight * ((double) totalEnergyLimit / totalEnergyWeight)); } diff --git a/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java b/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java index 2488686bfb0..ece16b25819 100644 --- a/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java +++ b/chainbase/src/main/java/org/tron/core/db/BandwidthProcessor.java @@ -437,7 +437,6 @@ public long calculateGlobalNetLimit(AccountCapsule accountCapsule) { if (frozeBalance < TRX_PRECISION) { return 0; } - long netWeight = frozeBalance / TRX_PRECISION; long totalNetLimit = chainBaseManager.getDynamicPropertiesStore().getTotalNetLimit(); long totalNetWeight = chainBaseManager.getDynamicPropertiesStore().getTotalNetWeight(); if (dynamicPropertiesStore.allowNewReward() && totalNetWeight <= 0) { @@ -446,16 +445,23 @@ public long calculateGlobalNetLimit(AccountCapsule accountCapsule) { if (totalNetWeight == 0) { return 0; } + if (hardenCalculation()) { + return calculateGlobalLimitV1(frozeBalance, totalNetLimit, totalNetWeight); + } + long netWeight = frozeBalance / TRX_PRECISION; return (long) (netWeight * ((double) totalNetLimit / totalNetWeight)); } public long calculateGlobalNetLimitV2(long frozeBalance) { - double netWeight = (double) frozeBalance / TRX_PRECISION; long totalNetLimit = dynamicPropertiesStore.getTotalNetLimit(); long totalNetWeight = dynamicPropertiesStore.getTotalNetWeight(); if (totalNetWeight == 0) { return 0; } + if (hardenCalculation()) { + return calculateGlobalLimitV2(frozeBalance, totalNetLimit, totalNetWeight); + } + double netWeight = (double) frozeBalance / TRX_PRECISION; return (long) (netWeight * ((double) totalNetLimit / totalNetWeight)); } diff --git a/chainbase/src/main/java/org/tron/core/db/EnergyProcessor.java b/chainbase/src/main/java/org/tron/core/db/EnergyProcessor.java index 30d778d0990..0c429178636 100644 --- a/chainbase/src/main/java/org/tron/core/db/EnergyProcessor.java +++ b/chainbase/src/main/java/org/tron/core/db/EnergyProcessor.java @@ -5,6 +5,7 @@ import static org.tron.core.config.Parameter.ChainConstant.BLOCK_PRODUCED_INTERVAL; import static org.tron.core.config.Parameter.ChainConstant.TRX_PRECISION; +import java.math.BigInteger; import lombok.extern.slf4j.Slf4j; import org.tron.common.parameter.CommonParameter; import org.tron.core.capsule.AccountCapsule; @@ -71,17 +72,20 @@ public void updateAdaptiveTotalEnergyLimit() { long result; if (totalEnergyAverageUsage > targetTotalEnergyLimit) { - result = totalEnergyCurrentLimit * AdaptiveResourceLimitConstants.CONTRACT_RATE_NUMERATOR - / AdaptiveResourceLimitConstants.CONTRACT_RATE_DENOMINATOR; - // logger.info(totalEnergyAverageUsage + ">" + targetTotalEnergyLimit + "\n" + result); + result = scaleByRate(totalEnergyCurrentLimit, + AdaptiveResourceLimitConstants.CONTRACT_RATE_NUMERATOR, + AdaptiveResourceLimitConstants.CONTRACT_RATE_DENOMINATOR); } else { - result = totalEnergyCurrentLimit * AdaptiveResourceLimitConstants.EXPAND_RATE_NUMERATOR - / AdaptiveResourceLimitConstants.EXPAND_RATE_DENOMINATOR; - // logger.info(totalEnergyAverageUsage + "<" + targetTotalEnergyLimit + "\n" + result); + result = scaleByRate(totalEnergyCurrentLimit, + AdaptiveResourceLimitConstants.EXPAND_RATE_NUMERATOR, + AdaptiveResourceLimitConstants.EXPAND_RATE_DENOMINATOR); } + long upperBound = hardenCalculation() + ? BigInteger.valueOf(totalEnergyLimit).multiply(BigInteger.valueOf( + dynamicPropertiesStore.getAdaptiveResourceLimitMultiplier())).longValueExact() + : totalEnergyLimit * dynamicPropertiesStore.getAdaptiveResourceLimitMultiplier(); result = min(max(result, totalEnergyLimit, this.disableJavaLangMath()), - totalEnergyLimit * dynamicPropertiesStore.getAdaptiveResourceLimitMultiplier(), - this.disableJavaLangMath()); + upperBound, this.disableJavaLangMath()); dynamicPropertiesStore.saveTotalEnergyCurrentLimit(result); logger.debug("Adjust totalEnergyCurrentLimit, old: {}, new: {}.", @@ -147,7 +151,6 @@ public long calculateGlobalEnergyLimit(AccountCapsule accountCapsule) { return 0; } - long energyWeight = frozeBalance / TRX_PRECISION; long totalEnergyLimit = dynamicPropertiesStore.getTotalEnergyCurrentLimit(); long totalEnergyWeight = dynamicPropertiesStore.getTotalEnergyWeight(); if (dynamicPropertiesStore.allowNewReward() && totalEnergyWeight <= 0) { @@ -155,16 +158,23 @@ public long calculateGlobalEnergyLimit(AccountCapsule accountCapsule) { } else { assert totalEnergyWeight > 0; } + if (hardenCalculation()) { + return calculateGlobalLimitV1(frozeBalance, totalEnergyLimit, totalEnergyWeight); + } + long energyWeight = frozeBalance / TRX_PRECISION; return (long) (energyWeight * ((double) totalEnergyLimit / totalEnergyWeight)); } public long calculateGlobalEnergyLimitV2(long frozeBalance) { - double energyWeight = (double) frozeBalance / TRX_PRECISION; long totalEnergyLimit = dynamicPropertiesStore.getTotalEnergyCurrentLimit(); long totalEnergyWeight = dynamicPropertiesStore.getTotalEnergyWeight(); if (totalEnergyWeight == 0) { return 0; } + if (hardenCalculation()) { + return calculateGlobalLimitV2(frozeBalance, totalEnergyLimit, totalEnergyWeight); + } + double energyWeight = (double) frozeBalance / TRX_PRECISION; return (long) (energyWeight * ((double) totalEnergyLimit / totalEnergyWeight)); } @@ -184,7 +194,15 @@ private long getHeadSlot() { return getHeadSlot(dynamicPropertiesStore); } - + private long scaleByRate(long value, long numerator, long denominator) { + if (hardenCalculation()) { + return BigInteger.valueOf(value) + .multiply(BigInteger.valueOf(numerator)) + .divide(BigInteger.valueOf(denominator)) + .longValueExact(); + } + return value * numerator / denominator; + } } diff --git a/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java b/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java index 7e170f9dab5..6706c430084 100644 --- a/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java +++ b/chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java @@ -3,8 +3,11 @@ import static org.tron.common.math.Maths.min; import static org.tron.common.math.Maths.round; import static org.tron.core.config.Parameter.ChainConstant.BLOCK_PRODUCED_INTERVAL; +import static org.tron.core.config.Parameter.ChainConstant.TRX_PRECISION; import static org.tron.core.config.Parameter.ChainConstant.WINDOW_SIZE_PRECISION; +import java.math.BigInteger; +import org.tron.common.math.StrictMathWrapper; import org.tron.common.utils.Commons; import org.tron.core.capsule.AccountCapsule; import org.tron.core.capsule.TransactionCapsule; @@ -45,8 +48,19 @@ protected long increase(long lastUsage, long usage, long lastTime, long now) { } protected long increase(long lastUsage, long usage, long lastTime, long now, long windowSize) { - long averageLastUsage = divideCeil(lastUsage * precision, windowSize); - long averageUsage = divideCeil(usage * precision, windowSize); + long averageLastUsage; + long averageUsage; + if (hardenCalculation()) { + BigInteger biPrecision = BigInteger.valueOf(precision); + BigInteger biWindowSize = BigInteger.valueOf(windowSize); + averageLastUsage = divideCeilExact( + BigInteger.valueOf(lastUsage).multiply(biPrecision), biWindowSize); + averageUsage = divideCeilExact( + BigInteger.valueOf(usage).multiply(biPrecision), biWindowSize); + } else { + averageLastUsage = divideCeil(lastUsage * precision, windowSize); + averageUsage = divideCeil(usage * precision, windowSize); + } if (lastTime != now) { assert now > lastTime; @@ -75,8 +89,20 @@ public long increase(AccountCapsule accountCapsule, ResourceCode resourceCode, return increaseV2(accountCapsule, resourceCode, lastUsage, usage, lastTime, now); } long oldWindowSize = accountCapsule.getWindowSize(resourceCode); - long averageLastUsage = divideCeil(lastUsage * this.precision, oldWindowSize); - long averageUsage = divideCeil(usage * this.precision, this.windowSize); + long averageLastUsage; + long averageUsage; + if (hardenCalculation()) { + BigInteger biPrecision = BigInteger.valueOf(this.precision); + averageLastUsage = divideCeilExact( + BigInteger.valueOf(lastUsage).multiply(biPrecision), + BigInteger.valueOf(oldWindowSize)); + averageUsage = divideCeilExact( + BigInteger.valueOf(usage).multiply(biPrecision), + BigInteger.valueOf(this.windowSize)); + } else { + averageLastUsage = divideCeil(lastUsage * this.precision, oldWindowSize); + averageUsage = divideCeil(usage * this.precision, this.windowSize); + } if (lastTime != now) { if (lastTime + oldWindowSize > now) { @@ -108,8 +134,20 @@ public long increaseV2(AccountCapsule accountCapsule, ResourceCode resourceCode, long lastUsage, long usage, long lastTime, long now) { long oldWindowSizeV2 = accountCapsule.getWindowSizeV2(resourceCode); long oldWindowSize = accountCapsule.getWindowSize(resourceCode); - long averageLastUsage = divideCeil(lastUsage * this.precision, oldWindowSize); - long averageUsage = divideCeil(usage * this.precision, this.windowSize); + long averageLastUsage; + long averageUsage; + if (hardenCalculation()) { + BigInteger biPrecision = BigInteger.valueOf(this.precision); + averageLastUsage = divideCeilExact( + BigInteger.valueOf(lastUsage).multiply(biPrecision), + BigInteger.valueOf(oldWindowSize)); + averageUsage = divideCeilExact( + BigInteger.valueOf(usage).multiply(biPrecision), + BigInteger.valueOf(this.windowSize)); + } else { + averageLastUsage = divideCeil(lastUsage * this.precision, oldWindowSize); + averageUsage = divideCeil(usage * this.precision, this.windowSize); + } if (lastTime != now) { if (lastTime + oldWindowSize > now) { @@ -130,8 +168,19 @@ public long increaseV2(AccountCapsule accountCapsule, ResourceCode resourceCode, } long remainWindowSize = oldWindowSizeV2 - (now - lastTime) * WINDOW_SIZE_PRECISION; - long newWindowSize = divideCeil( - remainUsage * remainWindowSize + usage * this.windowSize * WINDOW_SIZE_PRECISION, newUsage); + long newWindowSize; + if (hardenCalculation()) { + BigInteger biNewWindowSize = BigInteger.valueOf(remainUsage) + .multiply(BigInteger.valueOf(remainWindowSize)) + .add(BigInteger.valueOf(usage) + .multiply(BigInteger.valueOf(this.windowSize)) + .multiply(BigInteger.valueOf(WINDOW_SIZE_PRECISION))); + newWindowSize = divideCeilExact(biNewWindowSize, BigInteger.valueOf(newUsage)); + } else { + newWindowSize = divideCeil( + remainUsage * remainWindowSize + usage * this.windowSize * WINDOW_SIZE_PRECISION, + newUsage); + } newWindowSize = min(newWindowSize, this.windowSize * WINDOW_SIZE_PRECISION, this.disableJavaLangMath()); accountCapsule.setNewWindowSizeV2(resourceCode, newWindowSize); @@ -191,10 +240,18 @@ public void unDelegateIncreaseV2(AccountCapsule owner, final AccountCapsule rece remainReceiverWindowSizeV2 = remainReceiverWindowSizeV2 < 0 ? 0 : remainReceiverWindowSizeV2; // calculate new windowSize - long newOwnerWindowSize = - divideCeil( - ownerUsage * remainOwnerWindowSizeV2 + transferUsage * remainReceiverWindowSizeV2, - newOwnerUsage); + long newOwnerWindowSize; + if (hardenCalculation()) { + BigInteger bi = BigInteger.valueOf(ownerUsage) + .multiply(BigInteger.valueOf(remainOwnerWindowSizeV2)) + .add(BigInteger.valueOf(transferUsage) + .multiply(BigInteger.valueOf(remainReceiverWindowSizeV2))); + newOwnerWindowSize = divideCeilExact(bi, BigInteger.valueOf(newOwnerUsage)); + } else { + newOwnerWindowSize = divideCeil( + ownerUsage * remainOwnerWindowSizeV2 + transferUsage * remainReceiverWindowSizeV2, + newOwnerUsage); + } newOwnerWindowSize = min(newOwnerWindowSize, this.windowSize * WINDOW_SIZE_PRECISION, this.disableJavaLangMath()); owner.setNewWindowSizeV2(resourceCode, newOwnerWindowSize); @@ -204,6 +261,11 @@ public void unDelegateIncreaseV2(AccountCapsule owner, final AccountCapsule rece private long getNewWindowSize(long lastUsage, long lastWindowSize, long usage, long windowSize, long newUsage) { + if (hardenCalculation()) { + BigInteger bi = BigInteger.valueOf(lastUsage).multiply(BigInteger.valueOf(lastWindowSize)) + .add(BigInteger.valueOf(usage).multiply(BigInteger.valueOf(windowSize))); + return bi.divide(BigInteger.valueOf(newUsage)).longValueExact(); + } return (lastUsage * lastWindowSize + usage * windowSize) / newUsage; } @@ -211,11 +273,29 @@ private long divideCeil(long numerator, long denominator) { return (numerator / denominator) + ((numerator % denominator) > 0 ? 1 : 0); } + private long divideCeilExact(BigInteger numerator, BigInteger denominator) { + BigInteger[] divRem = numerator.divideAndRemainder(denominator); + long result = divRem[0].longValueExact(); + if (divRem[1].signum() > 0) { + result = StrictMathWrapper.addExact(result, 1); + } + return result; + } + private long getUsage(long usage, long windowSize) { + if (hardenCalculation()) { + return BigInteger.valueOf(usage).multiply(BigInteger.valueOf(windowSize)) + .divide(BigInteger.valueOf(precision)).longValueExact(); + } return usage * windowSize / precision; } private long getUsage(long oldUsage, long oldWindowSize, long newUsage, long newWindowSize) { + if (hardenCalculation()) { + BigInteger bi = BigInteger.valueOf(oldUsage).multiply(BigInteger.valueOf(oldWindowSize)) + .add(BigInteger.valueOf(newUsage).multiply(BigInteger.valueOf(newWindowSize))); + return bi.divide(BigInteger.valueOf(precision)).longValueExact(); + } return (oldUsage * oldWindowSize + newUsage * newWindowSize) / precision; } @@ -262,4 +342,38 @@ protected boolean consumeFeeForNewAccount(AccountCapsule accountCapsule, long fe protected boolean disableJavaLangMath() { return dynamicPropertiesStore.disableJavaLangMath(); } + + protected boolean hardenCalculation() { + return dynamicPropertiesStore.allowHardenResourceCalculation(); + } + + protected long calculateGlobalLimitV1(long frozeBalance, + long totalLimit, long totalWeight) { + long weight = frozeBalance / TRX_PRECISION; + return BigInteger.valueOf(weight) + .multiply(BigInteger.valueOf(totalLimit)) + .divide(BigInteger.valueOf(totalWeight)) + .longValueExact(); + } + + /** + * Hardened replacement of legacy V2 formula + * {@code (long)(((double) frozeBalance / TRX_PRECISION) + * * ((double) totalLimit / totalWeight))}. + * + *

Preserves V2 semantics: equivalent to + * {@code (frozeBalance * totalLimit) / (TRX_PRECISION * totalWeight)} with + * a single integer truncation at the end. Critically, fractional weight + * (i.e. {@code frozeBalance < TRX_PRECISION}) is preserved through the + * multiplication and only truncated at the final divide, so small balances + * yield the same proportional result as the double-arithmetic path. + */ + protected long calculateGlobalLimitV2(long frozeBalance, + long totalLimit, long totalWeight) { + return BigInteger.valueOf(frozeBalance) + .multiply(BigInteger.valueOf(totalLimit)) + .divide(BigInteger.valueOf(TRX_PRECISION) + .multiply(BigInteger.valueOf(totalWeight))) + .longValueExact(); + } } 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 e0adb0d444a..12dfca5e59a 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -240,6 +240,9 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking private static final byte[] ALLOW_TVM_OSAKA = "ALLOW_TVM_OSAKA".getBytes(); + private static final byte[] ALLOW_HARDEN_RESOURCE_CALCULATION = + "ALLOW_HARDEN_RESOURCE_CALCULATION".getBytes(); + @Autowired private DynamicPropertiesStore(@Value("properties") String dbName) { super(dbName); @@ -2993,6 +2996,21 @@ public void saveAllowTvmOsaka(long value) { this.put(ALLOW_TVM_OSAKA, new BytesCapsule(ByteArray.fromLong(value))); } + public long getAllowHardenResourceCalculation() { + return Optional.ofNullable(getUnchecked(ALLOW_HARDEN_RESOURCE_CALCULATION)) + .map(BytesCapsule::getData) + .map(ByteArray::toLong) + .orElse(0L); + } + + public void saveAllowHardenResourceCalculation(long value) { + this.put(ALLOW_HARDEN_RESOURCE_CALCULATION, new BytesCapsule(ByteArray.fromLong(value))); + } + + public boolean allowHardenResourceCalculation() { + return getAllowHardenResourceCalculation() == 1L; + } + private static class DynamicResourceProperties { private static final byte[] ONE_DAY_NET_LIMIT = "ONE_DAY_NET_LIMIT".getBytes(); 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 1a7f0c058a4..94c1e50284e 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 @@ -63,6 +63,8 @@ public class VMConfig { private static boolean ALLOW_TVM_OSAKA = false; + private static boolean ALLOW_HARDEN_RESOURCE_CALCULATION = false; + private VMConfig() { } @@ -178,6 +180,10 @@ public static void initAllowTvmOsaka(long allow) { ALLOW_TVM_OSAKA = allow == 1; } + public static void initAllowHardenResourceCalculation(long allow) { + ALLOW_HARDEN_RESOURCE_CALCULATION = allow == 1; + } + public static boolean getEnergyLimitHardFork() { return CommonParameter.ENERGY_LIMIT_HARD_FORK; } @@ -281,4 +287,8 @@ public static boolean allowTvmSelfdestructRestriction() { public static boolean allowTvmOsaka() { return ALLOW_TVM_OSAKA; } + + public static boolean allowHardenResourceCalculation() { + return ALLOW_HARDEN_RESOURCE_CALCULATION; + } } diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 39e8f06c281..954c381518d 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -1514,6 +1514,11 @@ public Protocol.ChainParameters getChainParameters() { .setValue(dbManager.getDynamicPropertiesStore().getAllowTvmOsaka()) .build()); + builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() + .setKey("getAllowHardenResourceCalculation") + .setValue(dbManager.getDynamicPropertiesStore().getAllowHardenResourceCalculation()) + .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 1bec0c2bda3..54e0c6fa362 100644 --- a/framework/src/main/java/org/tron/core/consensus/ProposalService.java +++ b/framework/src/main/java/org/tron/core/consensus/ProposalService.java @@ -396,6 +396,11 @@ public static boolean process(Manager manager, ProposalCapsule proposalCapsule) manager.getDynamicPropertiesStore().saveAllowTvmOsaka(entry.getValue()); break; } + case ALLOW_HARDEN_RESOURCE_CALCULATION: { + manager.getDynamicPropertiesStore() + .saveAllowHardenResourceCalculation(entry.getValue()); + break; + } default: find = false; break; diff --git a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java index c26da5a7bf7..2a9de1ea3b1 100644 --- a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java +++ b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java @@ -343,6 +343,8 @@ public void validateCheck() { testAllowTvmSelfdestructRestrictionProposal(); + testAllowHardenResourceCalculationProposal(); + forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.ENERGY_LIMIT.getValue(), stats); forkUtils.reset(); @@ -569,6 +571,48 @@ private void testAllowTvmSelfdestructRestrictionProposal() { e3.getMessage()); } + private void testAllowHardenResourceCalculationProposal() { + byte[] stats = new byte[27]; + forkUtils.getManager().getDynamicPropertiesStore() + .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_1.getValue(), stats); + ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_HARDEN_RESOURCE_CALCULATION.getCode(), 1)); + Assert.assertEquals( + "Bad chain parameter id [ALLOW_HARDEN_RESOURCE_CALCULATION]", + e1.getMessage()); + + long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() + .getMaintenanceTimeInterval(); + + long hardForkTime = + ((ForkBlockVersionEnum.VERSION_4_8_2.getHardForkTime() - 1) / maintenanceTimeInterval + 1) + * maintenanceTimeInterval; + forkUtils.getManager().getDynamicPropertiesStore() + .saveLatestBlockHeaderTimestamp(hardForkTime + 1); + + stats = new byte[27]; + Arrays.fill(stats, (byte) 1); + forkUtils.getManager().getDynamicPropertiesStore() + .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_2.getValue(), stats); + + // Should fail because the proposal value is invalid + ContractValidateException e2 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_HARDEN_RESOURCE_CALCULATION.getCode(), 2)); + Assert.assertEquals( + "This value[ALLOW_HARDEN_RESOURCE_CALCULATION] is only allowed to be 1", + e2.getMessage()); + + dynamicPropertiesStore.saveAllowHardenResourceCalculation(1); + ContractValidateException e3 = Assert.assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_HARDEN_RESOURCE_CALCULATION.getCode(), 1)); + Assert.assertEquals( + "[ALLOW_HARDEN_RESOURCE_CALCULATION] has been valid, no need to propose again", + e3.getMessage()); + } + private void testAllowMarketTransaction() { ThrowingRunnable off = () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, ProposalType.ALLOW_MARKET_TRANSACTION.getCode(), 0); diff --git a/framework/src/test/java/org/tron/core/db/CalculateGlobalLimitHardenTest.java b/framework/src/test/java/org/tron/core/db/CalculateGlobalLimitHardenTest.java new file mode 100644 index 00000000000..1df362fff42 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/CalculateGlobalLimitHardenTest.java @@ -0,0 +1,346 @@ +package org.tron.core.db; + +import com.google.protobuf.ByteString; +import java.math.BigInteger; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.common.utils.ByteArray; +import org.tron.core.Wallet; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.config.args.Args; +import org.tron.protos.Protocol.AccountType; + +@Slf4j +public class CalculateGlobalLimitHardenTest extends BaseTest { + + private static final String OWNER_ADDRESS; + + static { + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); + OWNER_ADDRESS = Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a1abc"; + } + + private EnergyProcessor energyProcessor; + private BandwidthProcessor bandwidthProcessor; + private AccountCapsule ownerCapsule; + + @Before + public void setUp() { + ownerCapsule = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)), + AccountType.Normal, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + energyProcessor = new EnergyProcessor( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + bandwidthProcessor = new BandwidthProcessor(dbManager.getChainBaseManager()); + } + + @After + public void tearDown() { + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); + } + + @Test + public void testGlobalEnergyLimitParity() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(2_000_000_000L); + ownerCapsule.setFrozenForEnergy(10_000_000_000L, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalEnergyLimitOverflowDetectedWithHardening() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(Long.MAX_VALUE / 2); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(1L); + ownerCapsule.setFrozenForEnergy(Long.MAX_VALUE / 4, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> energyProcessor.calculateGlobalEnergyLimit(ownerCapsule)); + } + + @Test + public void testGlobalEnergyLimitV2Parity() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(2_000_000_000L); + long frozeBalance = 10_000_000_000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalEnergyLimitV2CorrectVsDoublePrecisionLoss() { + long totalEnergyLimit = 50_000_000_000L; + long totalEnergyWeight = 1_234_567L; + long frozeBalance = 9_876_543_210_000_000L; // ~9.8e15 + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + + BigInteger expected = BigInteger.valueOf(frozeBalance) + .multiply(BigInteger.valueOf(totalEnergyLimit)) + .divide(BigInteger.valueOf(1_000_000L) + .multiply(BigInteger.valueOf(totalEnergyWeight))); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long actual = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + Assert.assertEquals(expected.longValueExact(), actual); + } + + @Test + public void testGlobalNetLimitParity() { + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(43_200_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(2_000_000_000L); + ownerCapsule.setFrozenForBandwidth(10_000_000_000L, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = bandwidthProcessor.calculateGlobalNetLimit(ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = bandwidthProcessor.calculateGlobalNetLimit(ownerCapsule); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalNetLimitOverflowDetectedWithHardening() { + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(Long.MAX_VALUE / 2); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(1L); + ownerCapsule.setFrozenForBandwidth(Long.MAX_VALUE / 4, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> bandwidthProcessor.calculateGlobalNetLimit(ownerCapsule)); + } + + + @Test + public void testGlobalNetLimitV2Parity() { + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(43_200_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(2_000_000_000L); + long frozeBalance = 10_000_000_000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalNetLimitV2ExactPrecision() { + long totalNetLimit = 43_200_000_000L; + long totalNetWeight = 1_234_567L; + long frozeBalance = 9_876_543_210_000_000L; + + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(totalNetLimit); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(totalNetWeight); + + BigInteger expected = BigInteger.valueOf(frozeBalance) + .multiply(BigInteger.valueOf(totalNetLimit)) + .divide(BigInteger.valueOf(1_000_000L) + .multiply(BigInteger.valueOf(totalNetWeight))); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long actual = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + Assert.assertEquals(expected.longValueExact(), actual); + } + + @Test + public void testGlobalEnergyLimitV2BelowTrxPrecisionMatchesDouble() { + long totalEnergyLimit = 50_000_000_000L; + long totalEnergyWeight = 2_000_000_000L; + long frozeBalance = 500_000L; // < TRX_PRECISION (1_000_000) + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + + Assert.assertEquals(12L, resultNew); + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGlobalNetLimitV2BelowTrxPrecisionMatchesDouble() { + long totalNetLimit = 43_200_000_000L; + long totalNetWeight = 2_000_000_000L; + long frozeBalance = 500_000L; // < TRX_PRECISION + + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(totalNetLimit); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(totalNetWeight); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + Assert.assertEquals(resultOld, resultNew); + Assert.assertTrue("non-zero proportional result expected", resultNew > 0); + } + + @Test + public void testGlobalEnergyLimitV1NonIntegerRatioParity() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); // force V1 path + long totalEnergyLimit = 50_000_000_000L; + long totalEnergyWeight = 1_234_567L; // not an exact divisor of totalEnergyLimit + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + ownerCapsule.setFrozenForEnergy(10_000_000_000L, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testV1FlooredWeightVsV2FractionalWeight() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(2_000_000_000L); + long frozeBalance = 1_500_000L; // 1.5 x TRX_PRECISION + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + // V1 path + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); + ownerCapsule.setFrozenForEnergy(frozeBalance, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + long v1New = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + + // Legacy V1 expectation: floor(1.5) * 25.0 = 1 * 25 = 25 + Assert.assertEquals(25L, v1New); + + // V2 path with the same balance keeps the fractional weight + long v2New = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + // Legacy V2 expectation: 1.5 * 25.0 = 37.5 -> 37 + Assert.assertEquals(37L, v2New); + + // And both must match their respective legacy doubles + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long v1Old = energyProcessor.calculateGlobalEnergyLimit(ownerCapsule); + long v2Old = energyProcessor.calculateGlobalEnergyLimitV2(frozeBalance); + Assert.assertEquals(v1Old, v1New); + Assert.assertEquals(v2Old, v2New); + } + + @Test + public void testGlobalNetLimitV1UsesTotalNetWeightNotLimit() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(0); // force V1 path + long totalNetLimit = 43_200_000_000L; + long totalNetWeight = 2_000_000_000L; // distinct from totalNetLimit + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(totalNetLimit); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(totalNetWeight); + long frozeBalance = 10_000_000_000L; + ownerCapsule.setFrozenForBandwidth(frozeBalance, 0L); + dbManager.getAccountStore().put(ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long actual = bandwidthProcessor.calculateGlobalNetLimit(ownerCapsule); + + Assert.assertEquals(216_000L, actual); + Assert.assertNotEquals(10_000L, actual); + } + + @Test + public void testGlobalNetLimitV2UsesTotalNetWeightNotLimit() { + long totalNetLimit = 43_200_000_000L; + long totalNetWeight = 2_000_000_000L; + dbManager.getDynamicPropertiesStore().saveTotalNetLimit(totalNetLimit); + dbManager.getDynamicPropertiesStore().saveTotalNetWeight(totalNetWeight); + long frozeBalance = 10_000_000_000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long actual = bandwidthProcessor.calculateGlobalNetLimitV2(frozeBalance); + + Assert.assertEquals(216_000L, actual); + Assert.assertNotEquals(10_000L, actual); + } + + + @Test + public void testUpdateAdaptiveTotalEnergyLimitParity() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyAverageUsage(20_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyTargetLimit(10_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveAdaptiveResourceLimitMultiplier(1000L); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + energyProcessor.updateAdaptiveTotalEnergyLimit(); + long resultOld = dbManager.getDynamicPropertiesStore().getTotalEnergyCurrentLimit(); + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(50_000_000_000L); + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + energyProcessor.updateAdaptiveTotalEnergyLimit(); + long resultNew = dbManager.getDynamicPropertiesStore().getTotalEnergyCurrentLimit(); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testUpdateAdaptiveTotalEnergyLimitOverflowDetected() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyAverageUsage(0L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyTargetLimit(Long.MAX_VALUE); + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit( + 10_000_000_000_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyLimit(10_000_000_000_000_000L); + dbManager.getDynamicPropertiesStore().saveAdaptiveResourceLimitMultiplier(1000L); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> energyProcessor.updateAdaptiveTotalEnergyLimit()); + } + + @Test + public void testUpdateAdaptiveLimitMultiplierOverflowDetected() { + dbManager.getDynamicPropertiesStore().saveTotalEnergyAverageUsage(0L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyTargetLimit(Long.MAX_VALUE); + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(1_000_000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyLimit(Long.MAX_VALUE / 100); + dbManager.getDynamicPropertiesStore().saveAdaptiveResourceLimitMultiplier(1000L); + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> energyProcessor.updateAdaptiveTotalEnergyLimit()); + } +} diff --git a/framework/src/test/java/org/tron/core/db/ResourceProcessorHardenTest.java b/framework/src/test/java/org/tron/core/db/ResourceProcessorHardenTest.java new file mode 100644 index 00000000000..ee096abd382 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/ResourceProcessorHardenTest.java @@ -0,0 +1,281 @@ +package org.tron.core.db; + +import com.google.protobuf.ByteString; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.common.utils.ByteArray; +import org.tron.core.Wallet; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.config.args.Args; +import org.tron.protos.Protocol.AccountType; +import org.tron.protos.contract.Common; +import org.tron.protos.contract.Common.ResourceCode; + +@Slf4j +public class ResourceProcessorHardenTest extends BaseTest { + + private static final String OWNER_ADDRESS; + private static final String RECEIVER_ADDRESS; + + static { + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); + OWNER_ADDRESS = + Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a1abc"; + RECEIVER_ADDRESS = + Wallet.getAddressPreFixString() + "abd4b9367799eaa3197fecb144eb71de1e049abc"; + } + + private EnergyProcessor processor; + private AccountCapsule ownerCapsule; + private AccountCapsule receiverCapsule; + + @Before + public void setUp() { + ownerCapsule = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString(OWNER_ADDRESS)), + AccountType.Normal, 10_000_000_000L); + + receiverCapsule = new AccountCapsule( + ByteString.copyFromUtf8("receiver"), + ByteString.copyFrom(ByteArray.fromHexString(RECEIVER_ADDRESS)), + AccountType.Normal, 10_000_000_000L); + + dbManager.getAccountStore().put( + ownerCapsule.getAddress().toByteArray(), ownerCapsule); + dbManager.getAccountStore().put( + receiverCapsule.getAddress().toByteArray(), receiverCapsule); + + dbManager.getDynamicPropertiesStore().saveLatestBlockHeaderTimestamp(10000L); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(10_000_000L); + + processor = new EnergyProcessor( + dbManager.getDynamicPropertiesStore(), dbManager.getAccountStore()); + } + + @Test + public void testIncreaseNormalValuesConsistent() { + long lastUsage = 1000L; + long usage = 500L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; // 24h in slots + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + long resultOld = processor.increase(lastUsage, usage, lastTime, now, windowSize); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long resultNew = processor.increase(lastUsage, usage, lastTime, now, windowSize); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testIncreaseV2NormalValuesConsistent() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowCancelAllUnfreezeV2(1); + + long lastUsage = 70_000_000L; + long usage = 2345L; + long lastTime = 9999L; + long now = 10000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + ownerCapsule.setNewWindowSize(Common.ResourceCode.ENERGY, 28800); + ownerCapsule.setWindowOptimized(Common.ResourceCode.ENERGY, true); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + long resultOld = processor.increaseV2(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + ownerCapsule.setNewWindowSize(Common.ResourceCode.ENERGY, 28800); + ownerCapsule.setWindowOptimized(Common.ResourceCode.ENERGY, true); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + long resultNew = processor.increaseV2(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testIncreaseOverflowDetectedWithHardening() { + long lastUsage = Long.MAX_VALUE / 10; // ~9.2e17 + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> processor.increase(lastUsage, usage, lastTime, now, windowSize)); + } + + @Test + public void testIncreaseOverflowSilentWithoutHardening() { + long lastUsage = Long.MAX_VALUE / 10; + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + processor.increase(lastUsage, usage, lastTime, now, windowSize); + } + + @Test + public void testIncreaseAcceptsIntermediateOverflowWhenResultFits() { + long lastUsage = Long.MAX_VALUE / 100; // ~9.2e16 + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long result = processor.increase(lastUsage, usage, lastTime, now, windowSize); + Assert.assertTrue("Result should be a valid long", result >= 0); + } + + @Test + public void testIncreaseWithAccountCapsuleConsistent() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + + long lastUsage = 5_000_000L; + long usage = 1_000L; + long lastTime = 9990L; + long now = 9995L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + ownerCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + long resultOld = processor.increase(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now); + long windowOld = ownerCapsule.getWindowSize(ResourceCode.ENERGY); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + ownerCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + long resultNew = processor.increase(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now); + long windowNew = ownerCapsule.getWindowSize(ResourceCode.ENERGY); + + Assert.assertEquals(resultOld, resultNew); + Assert.assertEquals(windowOld, windowNew); + } + + @Test + public void testUnDelegateIncreaseV2NormalValuesConsistent() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowCancelAllUnfreezeV2(1); + + long transferUsage = 1000L; + long now = 10000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + setupForUnDelegate(now); + processor.unDelegateIncreaseV2(ownerCapsule, receiverCapsule, + transferUsage, ResourceCode.ENERGY, now); + long usageOld = ownerCapsule.getUsage(ResourceCode.ENERGY); + long windowOld = ownerCapsule.getWindowSizeV2(ResourceCode.ENERGY); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + setupForUnDelegate(now); + processor.unDelegateIncreaseV2(ownerCapsule, receiverCapsule, + transferUsage, ResourceCode.ENERGY, now); + long usageNew = ownerCapsule.getUsage(ResourceCode.ENERGY); + long windowNew = ownerCapsule.getWindowSizeV2(ResourceCode.ENERGY); + + Assert.assertEquals(usageOld, usageNew); + Assert.assertEquals(windowOld, windowNew); + } + + @Test + public void testUnDelegateIncreaseV2ConsistentWithHardening() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowCancelAllUnfreezeV2(1); + + long transferUsage = 5_000_000L; + long now = 10000L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(0); + setupForUnDelegateWithUsage(now, 2_000_000L, 3_000_000L); + processor.unDelegateIncreaseV2(ownerCapsule, receiverCapsule, + transferUsage, ResourceCode.ENERGY, now); + long usageOld = ownerCapsule.getUsage(ResourceCode.ENERGY); + long windowOld = ownerCapsule.getWindowSizeV2(ResourceCode.ENERGY); + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + setupForUnDelegateWithUsage(now, 2_000_000L, 3_000_000L); + processor.unDelegateIncreaseV2(ownerCapsule, receiverCapsule, + transferUsage, ResourceCode.ENERGY, now); + long usageNew = ownerCapsule.getUsage(ResourceCode.ENERGY); + long windowNew = ownerCapsule.getWindowSizeV2(ResourceCode.ENERGY); + + Assert.assertEquals(usageOld, usageNew); + Assert.assertEquals(windowOld, windowNew); + } + + @Test + public void testIncreaseV2OverflowDetected() { + dbManager.getDynamicPropertiesStore().saveUnfreezeDelayDays(14); + dbManager.getDynamicPropertiesStore().saveAllowCancelAllUnfreezeV2(1); + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + + long lastUsage = Long.MAX_VALUE / 10; // ~9.2e17, above threshold + long usage = 1000L; + long lastTime = 9999L; + long now = 10000L; + + ownerCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + ownerCapsule.setWindowOptimized(ResourceCode.ENERGY, true); + ownerCapsule.setLatestConsumeTimeForEnergy(lastTime); + ownerCapsule.setEnergyUsage(lastUsage); + dbManager.getAccountStore().put( + ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + Assert.assertThrows(ArithmeticException.class, + () -> processor.increaseV2(ownerCapsule, ResourceCode.ENERGY, + lastUsage, usage, lastTime, now)); + } + + @Test + public void testLargeButSafeValuesWithHardening() { + long lastUsage = 300_000_000_000L; // 300 billion + long usage = 100L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + dbManager.getDynamicPropertiesStore().saveAllowHardenResourceCalculation(1); + long result = processor.increase(lastUsage, usage, lastTime, now, windowSize); + Assert.assertTrue("Result should be positive", result > 0); + } + + private void setupForUnDelegate(long now) { + setupForUnDelegateWithUsage(now, 5_000_000L, 3_000_000L); + } + + private void setupForUnDelegateWithUsage(long now, long ownerUsage, long receiverUsage) { + ownerCapsule.setLatestConsumeTimeForEnergy(now); + ownerCapsule.setEnergyUsage(ownerUsage); + ownerCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + ownerCapsule.setWindowOptimized(ResourceCode.ENERGY, true); + dbManager.getAccountStore().put( + ownerCapsule.getAddress().toByteArray(), ownerCapsule); + + receiverCapsule.setLatestConsumeTimeForEnergy(now - 100); + receiverCapsule.setEnergyUsage(receiverUsage); + receiverCapsule.setNewWindowSize(ResourceCode.ENERGY, 28800); + receiverCapsule.setWindowOptimized(ResourceCode.ENERGY, true); + dbManager.getAccountStore().put( + receiverCapsule.getAddress().toByteArray(), receiverCapsule); + } +} 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 new file mode 100644 index 00000000000..6b15409edd6 --- /dev/null +++ b/framework/src/test/java/org/tron/core/vm/repository/RepositoryImplHardenTest.java @@ -0,0 +1,280 @@ +package org.tron.core.vm.repository; + +import com.google.protobuf.ByteString; +import java.lang.reflect.Method; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.common.utils.ByteArray; +import org.tron.core.Wallet; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.config.args.Args; +import org.tron.core.store.StoreFactory; +import org.tron.core.vm.config.VMConfig; +import org.tron.protos.Protocol.AccountType; + +@Slf4j +public class RepositoryImplHardenTest extends BaseTest { + + static { + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); + } + + private RepositoryImpl repository; + private Method increaseMethod; + private Method getUsageMethod; + private Method usageToBalanceMethod; + + @Before + public void setUp() throws Exception { + repository = RepositoryImpl.createRoot(StoreFactory.getInstance()); + + increaseMethod = RepositoryImpl.class.getDeclaredMethod( + "increase", long.class, long.class, long.class, long.class, long.class); + increaseMethod.setAccessible(true); + + getUsageMethod = RepositoryImpl.class.getDeclaredMethod( + "getUsage", long.class, long.class); + getUsageMethod.setAccessible(true); + + usageToBalanceMethod = RepositoryImpl.class.getDeclaredMethod( + "usageToBalance", long.class, long.class, long.class); + usageToBalanceMethod.setAccessible(true); + } + + @After + public void tearDown() { + VMConfig.initAllowHardenResourceCalculation(0); + } + + private long invokeIncrease(long lastUsage, long usage, long lastTime, + long now, long windowSize) throws Exception { + try { + return (long) increaseMethod.invoke( + repository, lastUsage, usage, lastTime, now, windowSize); + } catch (java.lang.reflect.InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + throw e; + } + } + + private long invokeGetUsage(long usage, long windowSize) throws Exception { + try { + return (long) getUsageMethod.invoke(repository, usage, windowSize); + } catch (java.lang.reflect.InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + throw e; + } + } + + private long invokeUsageToBalance(long usage, long totalWeight, long totalLimit) + throws Exception { + try { + return (long) usageToBalanceMethod.invoke( + repository, usage, totalWeight, totalLimit); + } catch (java.lang.reflect.InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + throw e; + } + } + + @Test + public void testIncreaseNormalValuesParity() throws Exception { + long lastUsage = 1_000L; + long usage = 500L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(0); + long resultOld = invokeIncrease(lastUsage, usage, lastTime, now, windowSize); + + VMConfig.initAllowHardenResourceCalculation(1); + long resultNew = invokeIncrease(lastUsage, usage, lastTime, now, windowSize); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testGetUsageNormalValuesParity() throws Exception { + long usage = 100_000L; + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(0); + long resultOld = invokeGetUsage(usage, windowSize); + + VMConfig.initAllowHardenResourceCalculation(1); + long resultNew = invokeGetUsage(usage, windowSize); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testIncreaseOverflowDetectedWithHardening() { + long lastUsage = Long.MAX_VALUE / 10; // ~9.2e17 + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> invokeIncrease(lastUsage, usage, lastTime, now, windowSize)); + } + + @Test + public void testIncreaseOverflowSilentWithoutHardening() throws Exception { + long lastUsage = Long.MAX_VALUE / 10; + long usage = 1L; + long lastTime = 9990L; + long now = 9995L; + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(0); + invokeIncrease(lastUsage, usage, lastTime, now, windowSize); + } + + @Test + public void testGetUsageCorrectAcrossOverflowBoundary() throws Exception { + long usage = Long.MAX_VALUE / 1000; // ~9.2e15 + long windowSize = 28800L; + + long expected = java.math.BigInteger.valueOf(usage) + .multiply(java.math.BigInteger.valueOf(windowSize)) + .divide(java.math.BigInteger.valueOf(1_000_000L)) + .longValueExact(); + + VMConfig.initAllowHardenResourceCalculation(1); + long actual = invokeGetUsage(usage, windowSize); + Assert.assertEquals(expected, actual); + + VMConfig.initAllowHardenResourceCalculation(0); + long wrapped = invokeGetUsage(usage, windowSize); + Assert.assertNotEquals(expected, wrapped); + } + + @Test + public void testGetUsageLargeButSafeWithHardening() throws Exception { + long usage = 500_000_000_000L; // 5e11 + long windowSize = 28800L; + + VMConfig.initAllowHardenResourceCalculation(1); + long expected = java.math.BigInteger.valueOf(usage) + .multiply(java.math.BigInteger.valueOf(windowSize)) + .divide(java.math.BigInteger.valueOf(1_000_000L)) + .longValueExact(); + + long actual = invokeGetUsage(usage, windowSize); + Assert.assertEquals(expected, actual); + } + + + @Test + public void testUsageToBalanceParity() throws Exception { + long usage = 1_000_000L; + long totalWeight = 2_000_000_000L; + long totalLimit = 50_000_000_000L; + + VMConfig.initAllowHardenResourceCalculation(0); + long resultOld = invokeUsageToBalance(usage, totalWeight, totalLimit); + + VMConfig.initAllowHardenResourceCalculation(1); + long resultNew = invokeUsageToBalance(usage, totalWeight, totalLimit); + + Assert.assertEquals(resultOld, resultNew); + } + + @Test + public void testUsageToBalanceCorrectAcrossDoublePrecision() throws Exception { + long usage = 100_000_000L; // 1e8 + long totalWeight = 100_000_000_000L; // 1e11 -> usage * weight = 1e19, beyond 2^53 + long totalLimit = 50_000_000_000L; + + java.math.BigInteger expected = java.math.BigInteger.valueOf(usage) + .multiply(java.math.BigInteger.valueOf(totalWeight)) + .multiply(java.math.BigInteger.valueOf(1_000_000L)) + .divide(java.math.BigInteger.valueOf(totalLimit)); + + VMConfig.initAllowHardenResourceCalculation(1); + long actual = invokeUsageToBalance(usage, totalWeight, totalLimit); + + Assert.assertEquals(expected.longValueExact(), actual); + } + + @Test + public void testUsageToBalanceOverflowDetectedWithHardening() { + long usage = 1_000_000_000L; + long totalWeight = 1_000_000_000_000L; + long totalLimit = 1L; + + VMConfig.initAllowHardenResourceCalculation(1); + + Assert.assertThrows(ArithmeticException.class, + () -> invokeUsageToBalance(usage, totalWeight, totalLimit)); + } + + @Test + public void testCalculateGlobalEnergyLimitHardenedParityWithNonIntegerRatio() { + long totalEnergyLimit = 50_000_000_000L; + long totalEnergyWeight = 1_234_567L; + long frozeBalance = 10_000_000_000L; + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + + AccountCapsule account = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString( + Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a1abc")), + AccountType.Normal, 0L); + account.setFrozenForEnergy(frozeBalance, 0L); + + VMConfig.initAllowHardenResourceCalculation(0); + long resultOld = repository.calculateGlobalEnergyLimit(account); + + VMConfig.initAllowHardenResourceCalculation(1); + long resultNew = repository.calculateGlobalEnergyLimit(account); + + long expected = java.math.BigInteger.valueOf(10000L) + .multiply(java.math.BigInteger.valueOf(totalEnergyLimit)) + .divide(java.math.BigInteger.valueOf(totalEnergyWeight)) + .longValueExact(); + Assert.assertEquals(expected, resultNew); + Assert.assertEquals(resultOld, resultNew); + + long buggy = 10000L * (totalEnergyLimit / totalEnergyWeight); + Assert.assertNotEquals(buggy, resultNew); + } + + @Test + public void testCalculateGlobalEnergyLimitHardenedOverflowDetected() { + long totalEnergyLimit = Long.MAX_VALUE / 2; + long totalEnergyWeight = 1L; + long frozeBalance = Long.MAX_VALUE / 4; + + dbManager.getDynamicPropertiesStore().saveTotalEnergyCurrentLimit(totalEnergyLimit); + dbManager.getDynamicPropertiesStore().saveTotalEnergyWeight(totalEnergyWeight); + + AccountCapsule account = new AccountCapsule( + ByteString.copyFromUtf8("owner"), + ByteString.copyFrom(ByteArray.fromHexString( + Wallet.getAddressPreFixString() + "548794500882809695a8a687866e76d4271a1abc")), + AccountType.Normal, 0L); + account.setFrozenForEnergy(frozeBalance, 0L); + + VMConfig.initAllowHardenResourceCalculation(1); + Assert.assertThrows(ArithmeticException.class, + () -> repository.calculateGlobalEnergyLimit(account)); + } +}