diff --git a/hedera-node/hedera-app/build.gradle.kts b/hedera-node/hedera-app/build.gradle.kts index 165a9f50f039..5d5ec948af60 100644 --- a/hedera-node/hedera-app/build.gradle.kts +++ b/hedera-node/hedera-app/build.gradle.kts @@ -31,6 +31,7 @@ mainModuleInfo { } testModuleInfo { + requires("com.fasterxml.jackson.databind") requires("com.hedera.node.app") requires("com.hedera.node.app.spi.test.fixtures") requires("com.hedera.node.config.test.fixtures") diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/throttle/ThrottleAccumulator.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/throttle/ThrottleAccumulator.java index dd917c0cce51..11d2a32c3f11 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/throttle/ThrottleAccumulator.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/throttle/ThrottleAccumulator.java @@ -20,19 +20,27 @@ import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CALL_LOCAL; import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CREATE; import static com.hedera.hapi.node.base.HederaFunctionality.CRYPTO_CREATE; +import static com.hedera.hapi.node.base.HederaFunctionality.CRYPTO_TRANSFER; import static com.hedera.hapi.node.base.HederaFunctionality.ETHEREUM_TRANSACTION; import static com.hedera.node.app.hapi.utils.ethereum.EthTxData.populateEthTxData; import static com.hedera.node.app.hapi.utils.sysfiles.domain.throttling.ScaleFactor.ONE_TO_ONE; import static com.hedera.node.app.service.evm.accounts.HederaEvmContractAliases.isMirror; import static com.hedera.node.app.service.mono.utils.EntityIdUtils.isOfEvmAddressSize; +import static com.hedera.node.app.service.schedule.impl.handlers.HandlerUtility.childAsOrdinary; import static com.hedera.node.app.service.token.AliasUtils.isAlias; import static com.hedera.node.app.service.token.AliasUtils.isSerializedProtoKey; +import static com.hedera.node.app.spi.HapiUtils.functionOf; +import static com.hedera.node.app.throttle.ThrottleAccumulator.ThrottleType.FRONTEND_THROTTLE; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountAmount; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.NftTransfer; +import com.hedera.hapi.node.base.SignatureMap; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.state.schedule.Schedule; import com.hedera.hapi.node.token.CryptoTransferTransactionBody; import com.hedera.hapi.node.token.TokenMintTransactionBody; import com.hedera.hapi.node.transaction.Query; @@ -44,8 +52,11 @@ import com.hedera.node.app.hapi.utils.throttles.DeterministicThrottle; import com.hedera.node.app.hapi.utils.throttles.GasLimitDeterministicThrottle; import com.hedera.node.app.service.mono.throttling.ThrottleReqsManager; +import com.hedera.node.app.service.schedule.ReadableScheduleStore; import com.hedera.node.app.service.token.ReadableAccountStore; +import com.hedera.node.app.spi.UnknownHederaFunctionality; import com.hedera.node.app.spi.throttle.HandleThrottleParser; +import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.state.HederaState; import com.hedera.node.app.workflows.TransactionInfo; import com.hedera.node.app.workflows.dispatcher.ReadableStoreFactory; @@ -54,6 +65,7 @@ import com.hedera.node.config.data.AutoCreationConfig; import com.hedera.node.config.data.ContractsConfig; import com.hedera.node.config.data.LazyCreationConfig; +import com.hedera.node.config.data.SchedulingConfig; import com.hedera.node.config.data.TokensConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; @@ -117,7 +129,7 @@ public boolean shouldThrottle( @NonNull final HederaState state) { resetLastAllowedUse(); lastTxnWasGasThrottled = false; - if (shouldThrottleTxn(txnInfo, consensusTime, state)) { + if (shouldThrottleTxn(false, txnInfo, consensusTime, state)) { reclaimLastAllowedUse(); return true; } @@ -145,7 +157,7 @@ public boolean shouldThrottle( return false; } - final var shouldThrottleByGas = + final boolean shouldThrottleByGas = configuration.getConfigData(ContractsConfig.class).throttleThrottleByGas(); resetLastAllowedUse(); @@ -230,8 +242,22 @@ public static boolean isGasThrottled(@NonNull final HederaFunctionality function return GAS_THROTTLED_FUNCTIONS.contains(function); } + /* + * Resets the usage for all underlying throttles. + */ + public void resetUsage() { + lastTxnWasGasThrottled = false; + activeThrottles.forEach(DeterministicThrottle::resetUsage); + if (gasThrottle != null) { + gasThrottle.resetUsage(); + } + } + private boolean shouldThrottleTxn( - @NonNull final TransactionInfo txnInfo, @NonNull final Instant now, @NonNull final HederaState state) { + final boolean isScheduled, + @NonNull final TransactionInfo txnInfo, + @NonNull final Instant now, + @NonNull final HederaState state) { final var function = txnInfo.functionality(); final var configuration = configProvider.getConfiguration(); @@ -242,12 +268,12 @@ private boolean shouldThrottleTxn( // exemption // but this is only possible for the case of triggered transactions which is not yet implemented (see // MonoMultiplierSources.java) - final var isPayerThrottleExempt = throttleExempt(txnInfo.payerID(), configuration); + final boolean isPayerThrottleExempt = throttleExempt(txnInfo.payerID(), configuration); if (isPayerThrottleExempt) { return false; } - final var txGasLimit = getGasLimitForContractTx(txnInfo.txBody(), txnInfo.functionality()); + final long txGasLimit = getGasLimitForContractTx(txnInfo.txBody(), txnInfo.functionality()); if (isGasExhausted(function, now, txGasLimit, configuration)) { lastTxnWasGasThrottled = true; return true; @@ -259,6 +285,20 @@ private boolean shouldThrottleTxn( } return switch (function) { + case SCHEDULE_CREATE -> { + if (isScheduled) { + throw new IllegalStateException("ScheduleCreate cannot be a child!"); + } + + yield shouldThrottleScheduleCreate(manager, txnInfo, now, state); + } + case SCHEDULE_SIGN -> { + if (isScheduled) { + throw new IllegalStateException("ScheduleSign cannot be a child!"); + } + + yield shouldThrottleScheduleSign(manager, txnInfo, now, state); + } case TOKEN_MINT -> shouldThrottleMint(manager, txnInfo.txBody().tokenMint(), now, configuration); case CRYPTO_TRANSFER -> { final var accountStore = new ReadableStoreFactory(state).getStore(ReadableAccountStore.class); @@ -274,11 +314,149 @@ yield shouldThrottleEthTxn( }; } + private boolean shouldThrottleScheduleCreate( + final ThrottleReqsManager manager, + final TransactionInfo txnInfo, + final Instant now, + final HederaState state) { + final var txnBody = txnInfo.txBody(); + final var scheduleCreate = txnBody.scheduleCreate(); + final var scheduled = scheduleCreate.scheduledTransactionBody(); + final var schedule = Schedule.newBuilder() + .originalCreateTransaction(txnBody) + .payerAccountId(txnInfo.payerID()) + .scheduledTransaction(scheduled) + .build(); + + TransactionBody innerTxn; + HederaFunctionality scheduledFunction; + try { + innerTxn = childAsOrdinary(schedule); + scheduledFunction = functionOf(innerTxn); + } catch (HandleException | UnknownHederaFunctionality ex) { + log.debug("ScheduleCreate was associated with an invalid txn.", ex); + return true; + } + + // maintain legacy behaviour + final var configuration = configProvider.getConfiguration(); + final boolean areLongTermSchedulesEnabled = + configuration.getConfigData(SchedulingConfig.class).longTermEnabled(); + if (!areLongTermSchedulesEnabled) { + final boolean isAutoCreationEnabled = + configuration.getConfigData(AutoCreationConfig.class).enabled(); + final boolean isLazyCreationEnabled = + configuration.getConfigData(LazyCreationConfig.class).enabled(); + + // we check for CryptoTransfer because implicit creations (i.e. auto- or lazy-creation) may happen in it, + // and we need to throttle those separately + if ((isAutoCreationEnabled || isLazyCreationEnabled) && scheduledFunction == CRYPTO_TRANSFER) { + final var transfer = scheduled.cryptoTransfer(); + if (usesAliases(transfer)) { + final var accountStore = new ReadableStoreFactory(state).getStore(ReadableAccountStore.class); + final var transferTxnBody = TransactionBody.newBuilder() + .cryptoTransfer(transfer) + .build(); + final int implicitCreationsCount = getImplicitCreationsCount(transferTxnBody, accountStore); + if (implicitCreationsCount > 0) { + return shouldThrottleImplicitCreations(implicitCreationsCount, now); + } + } + } + return !manager.allReqsMetAt(now); + } else { + log.warn("Long term scheduling is enabled, but throttling of long term schedules is not yet implemented."); + if (!manager.allReqsMetAt(now)) { + return true; + } + + // only check deeply if the schedule could immediately execute + if ((!scheduleCreate.waitForExpiry()) && (throttleType == FRONTEND_THROTTLE)) { + var effectivePayer = scheduleCreate.hasPayerAccountID() + ? scheduleCreate.payerAccountID() + : txnBody.transactionID().accountID(); + + final var innerTxnInfo = new TransactionInfo( + Transaction.DEFAULT, + innerTxn, + TransactionID.DEFAULT, + effectivePayer, + SignatureMap.DEFAULT, + Bytes.EMPTY, + scheduledFunction); + + return shouldThrottleTxn(true, innerTxnInfo, now, state); + } + + return false; + } + } + + private boolean shouldThrottleScheduleSign( + ThrottleReqsManager manager, TransactionInfo txnInfo, Instant now, HederaState state) { + final var txnBody = txnInfo.txBody(); + if (!manager.allReqsMetAt(now)) { + return true; + } + + // maintain legacy behaviour + final var configuration = configProvider.getConfiguration(); + final boolean areLongTermSchedulesEnabled = + configuration.getConfigData(SchedulingConfig.class).longTermEnabled(); + if (!areLongTermSchedulesEnabled) { + return false; + } else { + log.warn("Long term scheduling is enabled, but throttling of long term schedules is not yet implemented."); + // deeply check throttle only in the frontend throttle + if (throttleType != FRONTEND_THROTTLE) { + return false; + } + + final var scheduledId = txnBody.scheduleSign().scheduleID(); + final var scheduleStore = new ReadableStoreFactory(state).getStore(ReadableScheduleStore.class); + final var schedule = scheduleStore.get(scheduledId); + if (schedule == null) { + log.error( + "Tried to throttle in the frontend throttle a ScheduleSign that does not exist! We should not get here."); + return true; + } + + // only check deeply if the schedule could immediately execute + if (schedule.waitForExpiry()) { + return false; + } + + TransactionBody innerTxn; + HederaFunctionality scheduledFunction; + try { + innerTxn = childAsOrdinary(schedule); + scheduledFunction = functionOf(innerTxn); + } catch (HandleException | UnknownHederaFunctionality ex) { + log.error("ScheduleSign was associated with an invalid txn.", ex); + return true; + } + + final var effectivePayer = + schedule.hasPayerAccountId() ? schedule.payerAccountId() : schedule.schedulerAccountId(); + + final var innerTxnInfo = new TransactionInfo( + Transaction.DEFAULT, + innerTxn, + TransactionID.DEFAULT, + effectivePayer, + SignatureMap.DEFAULT, + Bytes.EMPTY, + scheduledFunction); + + return shouldThrottleTxn(true, innerTxnInfo, now, state); + } + } + public static boolean throttleExempt( @NonNull final AccountID accountID, @NonNull final Configuration configuration) { - final var maxThrottleExemptNum = + final long maxThrottleExemptNum = configuration.getConfigData(AccountsConfig.class).lastThrottleExempt(); - final var accountNum = accountID.accountNum(); + final long accountNum = accountID.accountNum().longValue(); return 1L <= accountNum && accountNum <= maxThrottleExemptNum; } @@ -313,7 +491,7 @@ private boolean isGasExhausted( @NonNull final Instant now, final long txGasLimit, @NonNull final Configuration configuration) { - final var shouldThrottleByGas = + final boolean shouldThrottleByGas = configuration.getConfigData(ContractsConfig.class).throttleThrottleByGas(); return shouldThrottleByGas && isGasThrottled(function) @@ -325,7 +503,7 @@ private boolean shouldThrottleMint( @NonNull final TokenMintTransactionBody op, @NonNull final Instant now, @NonNull final Configuration configuration) { - final var numNfts = op.metadata().size(); + final int numNfts = op.metadata().size(); if (numNfts == 0) { return !manager.allReqsMetAt(now); } else { @@ -340,9 +518,9 @@ private boolean shouldThrottleCryptoTransfer( @NonNull final Instant now, @NonNull final Configuration configuration, final int implicitCreationsCount) { - final var isAutoCreationEnabled = + final boolean isAutoCreationEnabled = configuration.getConfigData(AutoCreationConfig.class).enabled(); - final var isLazyCreationEnabled = + final boolean isLazyCreationEnabled = configuration.getConfigData(LazyCreationConfig.class).enabled(); if (isAutoCreationEnabled || isLazyCreationEnabled) { return shouldThrottleBasedOnImplicitCreations(manager, implicitCreationsCount, now); @@ -356,9 +534,9 @@ private boolean shouldThrottleEthTxn( @NonNull final Instant now, @NonNull final Configuration configuration, final int implicitCreationsCount) { - final var isAutoCreationEnabled = + final boolean isAutoCreationEnabled = configuration.getConfigData(AutoCreationConfig.class).enabled(); - final var isLazyCreationEnabled = + final boolean isLazyCreationEnabled = configuration.getConfigData(LazyCreationConfig.class).enabled(); if (isAutoCreationEnabled && isLazyCreationEnabled) { return shouldThrottleBasedOnImplicitCreations(manager, implicitCreationsCount, now); @@ -369,7 +547,7 @@ private boolean shouldThrottleEthTxn( private int getImplicitCreationsCount( @NonNull final TransactionBody txnBody, @NonNull final ReadableAccountStore accountStore) { - var implicitCreationsCount = 0; + int implicitCreationsCount = 0; if (txnBody.hasEthereumTransaction()) { final var ethTxData = populateEthTxData( txnBody.ethereumTransaction().ethereumData().toByteArray()); @@ -377,7 +555,7 @@ private int getImplicitCreationsCount( return UNKNOWN_NUM_IMPLICIT_CREATIONS; } - final var doesNotExist = accountStore.containsAlias(Bytes.wrap(ethTxData.to())); + final boolean doesNotExist = accountStore.containsAlias(Bytes.wrap(ethTxData.to())); if (doesNotExist && ethTxData.value().compareTo(BigInteger.ZERO) > 0) { implicitCreationsCount++; } @@ -402,7 +580,7 @@ private int hbarAdjustsImplicitCreationsCount( return 0; } - var implicitCreationsCount = 0; + int implicitCreationsCount = 0; for (var adjust : cryptoTransferBody.transfers().accountAmounts()) { if (!isKnownAlias(adjust.accountID(), accountStore) && containsImplicitCreations(adjust)) { implicitCreationsCount++; @@ -419,7 +597,7 @@ private int tokenAdjustsImplicitCreationsCount( return 0; } - var implicitCreationsCount = 0; + int implicitCreationsCount = 0; for (var tokenAdjust : cryptoTransferBody.tokenTransfers()) { for (final var adjust : tokenAdjust.transfers()) { if (!isKnownAlias(adjust.accountID(), accountStore) && containsImplicitCreations(adjust)) { @@ -437,6 +615,29 @@ private int tokenAdjustsImplicitCreationsCount( return implicitCreationsCount; } + private boolean usesAliases(final CryptoTransferTransactionBody transferBody) { + for (var adjust : transferBody.transfers().accountAmounts()) { + if (isAlias(adjust.accountID())) { + return true; + } + } + + for (var tokenAdjusts : transferBody.tokenTransfers()) { + for (var ownershipChange : tokenAdjusts.nftTransfers()) { + if (isAlias(ownershipChange.senderAccountID()) || isAlias(ownershipChange.receiverAccountID())) { + return true; + } + } + for (var tokenAdjust : tokenAdjusts.transfers()) { + if (isAlias(tokenAdjust.accountID())) { + return true; + } + } + } + + return false; + } + private boolean isKnownAlias(@NonNull final AccountID idOrAlias, @NonNull final ReadableAccountStore accountStore) { if (isAlias(idOrAlias)) { final var alias = idOrAlias.alias(); diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/throttle/ThrottleAccumulatorTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/throttle/ThrottleAccumulatorTest.java new file mode 100644 index 000000000000..94b9acc2b04e --- /dev/null +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/throttle/ThrottleAccumulatorTest.java @@ -0,0 +1,1081 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.throttle; + +import static com.hedera.hapi.node.base.HederaFunctionality.CONSENSUS_SUBMIT_MESSAGE; +import static com.hedera.hapi.node.base.HederaFunctionality.CRYPTO_TRANSFER; +import static com.hedera.hapi.node.base.HederaFunctionality.SCHEDULE_CREATE; +import static com.hedera.hapi.node.base.HederaFunctionality.SCHEDULE_SIGN; +import static com.hedera.node.app.service.schedule.impl.ScheduleServiceImpl.SCHEDULES_BY_ID_KEY; +import static com.hedera.node.app.throttle.ThrottleAccumulator.ThrottleType.FRONTEND_THROTTLE; +import static com.hedera.pbj.runtime.ProtoTestTools.getThreadLocalDataBuffer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hedera.hapi.node.base.AccountAmount; +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.Key; +import com.hedera.hapi.node.base.ScheduleID; +import com.hedera.hapi.node.base.SignatureMap; +import com.hedera.hapi.node.base.Transaction; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.base.TransferList; +import com.hedera.hapi.node.consensus.ConsensusSubmitMessageTransactionBody; +import com.hedera.hapi.node.scheduled.SchedulableTransactionBody; +import com.hedera.hapi.node.scheduled.ScheduleCreateTransactionBody; +import com.hedera.hapi.node.scheduled.ScheduleSignTransactionBody; +import com.hedera.hapi.node.state.schedule.Schedule; +import com.hedera.hapi.node.token.CryptoTransferTransactionBody; +import com.hedera.hapi.node.transaction.ThrottleDefinitions; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.hapi.utils.throttles.BucketThrottle; +import com.hedera.node.app.spi.fixtures.util.LogCaptor; +import com.hedera.node.app.spi.fixtures.util.LogCaptureExtension; +import com.hedera.node.app.spi.fixtures.util.LoggingSubject; +import com.hedera.node.app.spi.fixtures.util.LoggingTarget; +import com.hedera.node.app.spi.state.ReadableKVState; +import com.hedera.node.app.spi.state.ReadableStates; +import com.hedera.node.app.state.HederaState; +import com.hedera.node.app.workflows.TransactionInfo; +import com.hedera.node.config.ConfigProvider; +import com.hedera.node.config.VersionedConfiguration; +import com.hedera.node.config.data.AccountsConfig; +import com.hedera.node.config.data.AutoCreationConfig; +import com.hedera.node.config.data.ContractsConfig; +import com.hedera.node.config.data.LazyCreationConfig; +import com.hedera.node.config.data.SchedulingConfig; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import java.io.IOException; +import java.io.InputStream; +import java.time.Instant; +import java.util.ArrayList; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; + +@ExtendWith({MockitoExtension.class, LogCaptureExtension.class}) +class ThrottleAccumulatorTest { + private static final int CAPACITY_SPLIT = 2; + private static final Instant CONSENSUS_NOW = Instant.ofEpochSecond(1_234_567L, 123); + private static final AccountID PAYER_ID = + AccountID.newBuilder().accountNum(1234L).build(); + private static final Key A_PRIMITIVE_KEY = Key.newBuilder() + .ed25519(Bytes.wrap("01234567890123456789012345678901")) + .build(); + private static final ScheduleID SCHEDULE_ID = + ScheduleID.newBuilder().scheduleNum(333333L).build(); + + @LoggingSubject + private ThrottleAccumulator subject; + + @LoggingTarget + private LogCaptor logCaptor; + + @Mock + private ConfigProvider configProvider; + + @Mock + private VersionedConfiguration configuration; + + @Mock + private SchedulingConfig schedulingConfig; + + @Mock + private AccountsConfig accountsConfig; + + @Mock + private ContractsConfig contractsConfig; + + @Mock + private AutoCreationConfig autoCreationConfig; + + @Mock + private LazyCreationConfig lazyCreationConfig; + + @Mock + private HederaState state; + + @Mock + private ReadableStates readableStates; + + @Mock + private ReadableKVState aliases; + + @Mock + private ReadableKVState schedules; + + @ParameterizedTest + @CsvSource({ + "FRONTEND_THROTTLE,true,true", + "FRONTEND_THROTTLE,true,false", + "FRONTEND_THROTTLE,false,true", + "FRONTEND_THROTTLE,false,false", + "BACKEND_THROTTLE,true,true", + "BACKEND_THROTTLE,true,false", + "BACKEND_THROTTLE,false,true", + "BACKEND_THROTTLE,false,false", + }) + @MockitoSettings(strictness = org.mockito.quality.Strictness.LENIENT) + void usesScheduleCreateThrottleForSubmitMessage( + final ThrottleAccumulator.ThrottleType throttleType, + final boolean longTermEnabled, + final boolean waitForExpiry) + throws IOException { + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, throttleType); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + given(configuration.getConfigData(SchedulingConfig.class)).willReturn(schedulingConfig); + given(schedulingConfig.longTermEnabled()).willReturn(longTermEnabled); + given(configuration.getConfigData(AutoCreationConfig.class)).willReturn(autoCreationConfig); + given(autoCreationConfig.enabled()).willReturn(true); + given(configuration.getConfigData(LazyCreationConfig.class)).willReturn(lazyCreationConfig); + given(lazyCreationConfig.enabled()).willReturn(false); + + final var scheduledSubmit = SchedulableTransactionBody.newBuilder() + .consensusSubmitMessage(ConsensusSubmitMessageTransactionBody.DEFAULT) + .build(); + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles.json"); + subject.rebuildFor(defs); + + // when + final var txnInfo = scheduleCreate(scheduledSubmit, waitForExpiry, null); + final boolean firstAns = subject.shouldThrottle(txnInfo, CONSENSUS_NOW, state); + boolean subsequentAns = false; + for (int i = 1; i <= 150; i++) { + subsequentAns = subject.shouldThrottle(txnInfo, CONSENSUS_NOW.plusNanos(i), state); + } + + final var throttlesNow = subject.activeThrottlesFor(SCHEDULE_CREATE); + final var aNow = throttlesNow.get(0); + + // then + assertFalse(firstAns); + assertTrue(subsequentAns); + assertEquals(149999992500000L, aNow.used()); + assertEquals( + longTermEnabled && throttleType == FRONTEND_THROTTLE && (!waitForExpiry) ? 149999255000000L : 0, + subject.activeThrottlesFor(CONSENSUS_SUBMIT_MESSAGE).get(0).used()); + } + + @ParameterizedTest + @CsvSource({ + "FRONTEND_THROTTLE,true,true", + "FRONTEND_THROTTLE,true,false", + "FRONTEND_THROTTLE,false,true", + "FRONTEND_THROTTLE,false,false", + "BACKEND_THROTTLE,true,true", + "BACKEND_THROTTLE,true,false", + "BACKEND_THROTTLE,false,true", + "BACKEND_THROTTLE,false,false", + }) + @MockitoSettings(strictness = org.mockito.quality.Strictness.LENIENT) + void usesScheduleCreateThrottleWithNestedThrottleExempt( + final ThrottleAccumulator.ThrottleType throttleType, + final boolean longTermEnabled, + final boolean waitForExpiry) + throws IOException { + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, throttleType); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + given(configuration.getConfigData(SchedulingConfig.class)).willReturn(schedulingConfig); + given(schedulingConfig.longTermEnabled()).willReturn(longTermEnabled); + given(configuration.getConfigData(AutoCreationConfig.class)).willReturn(autoCreationConfig); + given(autoCreationConfig.enabled()).willReturn(true); + given(configuration.getConfigData(LazyCreationConfig.class)).willReturn(lazyCreationConfig); + given(lazyCreationConfig.enabled()).willReturn(false); + + final var scheduledSubmit = SchedulableTransactionBody.newBuilder() + .consensusSubmitMessage(ConsensusSubmitMessageTransactionBody.DEFAULT) + .build(); + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles.json"); + subject.rebuildFor(defs); + + // when + final var txnInfo = scheduleCreate( + scheduledSubmit, + waitForExpiry, + AccountID.newBuilder().accountNum(2L).build()); + final boolean firstAns = subject.shouldThrottle(txnInfo, CONSENSUS_NOW, state); + boolean subsequentAns = false; + for (int i = 1; i <= 150; i++) { + subsequentAns = subject.shouldThrottle(txnInfo, CONSENSUS_NOW.plusNanos(i), state); + } + + final var throttlesNow = subject.activeThrottlesFor(SCHEDULE_CREATE); + final var aNow = throttlesNow.get(0); + + // then + assertFalse(firstAns); + assertTrue(subsequentAns); + assertEquals(149999992500000L, aNow.used()); + assertEquals( + 0, subject.activeThrottlesFor(CONSENSUS_SUBMIT_MESSAGE).get(0).used()); + } + + @ParameterizedTest + @EnumSource + void scheduleCreateAlwaysThrottledWhenNoBody(final ThrottleAccumulator.ThrottleType throttleType) + throws IOException { + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, throttleType); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles.json"); + subject.rebuildFor(defs); + + // when + final var txnInfo = scheduleCreate(SchedulableTransactionBody.DEFAULT, false, null); + final boolean firstAns = subject.shouldThrottle(txnInfo, CONSENSUS_NOW, state); + for (int i = 1; i <= 150; i++) { + assertTrue(subject.shouldThrottle(txnInfo, CONSENSUS_NOW.plusNanos(i), state)); + } + + final var throttlesNow = subject.activeThrottlesFor(SCHEDULE_CREATE); + final var aNow = throttlesNow.get(0); + + // then + assertTrue(firstAns); + assertEquals(0, aNow.used()); + assertEquals( + 0, subject.activeThrottlesFor(CONSENSUS_SUBMIT_MESSAGE).get(0).used()); + } + + @ParameterizedTest + @CsvSource({ + "FRONTEND_THROTTLE,true", + "FRONTEND_THROTTLE,false", + "BACKEND_THROTTLE,true", + "BACKEND_THROTTLE,false", + }) + @MockitoSettings(strictness = org.mockito.quality.Strictness.LENIENT) + void usesScheduleCreateThrottleForCryptoTransferNoAutoCreations( + final ThrottleAccumulator.ThrottleType throttleType, final boolean longTermEnabled) throws IOException { + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, throttleType); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + given(configuration.getConfigData(SchedulingConfig.class)).willReturn(schedulingConfig); + given(schedulingConfig.longTermEnabled()).willReturn(longTermEnabled); + given(configuration.getConfigData(AutoCreationConfig.class)).willReturn(autoCreationConfig); + given(autoCreationConfig.enabled()).willReturn(true); + given(configuration.getConfigData(LazyCreationConfig.class)).willReturn(lazyCreationConfig); + given(lazyCreationConfig.enabled()).willReturn(false); + + given(state.createReadableStates(any())).willReturn(readableStates); + + final var scheduledTransferNoAliases = SchedulableTransactionBody.newBuilder() + .cryptoTransfer(cryptoTransferWithImplicitCreations(0)) + .build(); + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles.json"); + subject.rebuildFor(defs); + + // when + final var txnInfo = scheduleCreate(scheduledTransferNoAliases, false, null); + final boolean ans = subject.shouldThrottle(txnInfo, CONSENSUS_NOW, state); + final var throttlesNow = subject.activeThrottlesFor(SCHEDULE_CREATE); + final var aNow = throttlesNow.get(0); + + // then + assertFalse(ans); + assertEquals(BucketThrottle.capacityUnitsPerTxn(), aNow.used()); + assertEquals( + longTermEnabled && throttleType == FRONTEND_THROTTLE ? BucketThrottle.capacityUnitsPerTxn() : 0, + subject.activeThrottlesFor(CRYPTO_TRANSFER).get(0).used()); + } + + @ParameterizedTest + @CsvSource({ + "FRONTEND_THROTTLE,true", + "FRONTEND_THROTTLE,false", + "BACKEND_THROTTLE,true", + "BACKEND_THROTTLE,false", + }) + @MockitoSettings(strictness = org.mockito.quality.Strictness.LENIENT) + void doesntUseCryptoCreateThrottleForCryptoTransferWithAutoCreationIfAutoAndLazyCreationDisabled( + final ThrottleAccumulator.ThrottleType throttleType, final boolean longTermEnabled) throws IOException { + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, throttleType); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + given(configuration.getConfigData(SchedulingConfig.class)).willReturn(schedulingConfig); + given(schedulingConfig.longTermEnabled()).willReturn(longTermEnabled); + given(configuration.getConfigData(AutoCreationConfig.class)).willReturn(autoCreationConfig); + given(autoCreationConfig.enabled()).willReturn(false); + given(configuration.getConfigData(LazyCreationConfig.class)).willReturn(lazyCreationConfig); + given(lazyCreationConfig.enabled()).willReturn(false); + + given(state.createReadableStates(any())).willReturn(readableStates); + given(readableStates.get(any())).willReturn(aliases); + + final var alias = keyToBytes(A_PRIMITIVE_KEY); + var accountAmounts = new ArrayList(); + accountAmounts.add(AccountAmount.newBuilder() + .amount(-1_000_000_000L) + .accountID(AccountID.newBuilder().accountNum(3333L).build()) + .build()); + accountAmounts.add(AccountAmount.newBuilder() + .amount(+1_000_000_000L) + .accountID(AccountID.newBuilder().alias(alias).build()) + .build()); + final var scheduledTransferWithAutoCreation = SchedulableTransactionBody.newBuilder() + .cryptoTransfer(CryptoTransferTransactionBody.newBuilder() + .transfers(TransferList.newBuilder() + .accountAmounts(accountAmounts) + .build())) + .build(); + + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles.json"); + subject.rebuildFor(defs); + + // when + final var txnInfo = scheduleCreate(scheduledTransferWithAutoCreation, false, null); + final boolean ans = subject.shouldThrottle(txnInfo, CONSENSUS_NOW, state); + final var throttlesNow = subject.activeThrottlesFor(SCHEDULE_CREATE); + final var aNow = throttlesNow.get(0); + + // then + assertFalse(ans); + assertEquals(BucketThrottle.capacityUnitsPerTxn(), aNow.used()); + + assertEquals( + longTermEnabled && throttleType == FRONTEND_THROTTLE ? BucketThrottle.capacityUnitsPerTxn() : 0, + subject.activeThrottlesFor(CRYPTO_TRANSFER).get(0).used()); + } + + @ParameterizedTest + @CsvSource({ + "FRONTEND_THROTTLE,true,true", + "FRONTEND_THROTTLE,true,false", + "FRONTEND_THROTTLE,false,true", + "FRONTEND_THROTTLE,false,false", + "BACKEND_THROTTLE,true,true", + "BACKEND_THROTTLE,true,false", + "BACKEND_THROTTLE,false,true", + "BACKEND_THROTTLE,false,false", + }) + @MockitoSettings(strictness = org.mockito.quality.Strictness.LENIENT) + void doesntUseCryptoCreateThrottleForCryptoTransferWithNoAliases( + final ThrottleAccumulator.ThrottleType throttleType, + final boolean longTermEnabled, + final boolean autoOrLazyCreationEnabled) + throws IOException { + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, throttleType); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + given(configuration.getConfigData(SchedulingConfig.class)).willReturn(schedulingConfig); + given(schedulingConfig.longTermEnabled()).willReturn(longTermEnabled); + given(configuration.getConfigData(AutoCreationConfig.class)).willReturn(autoCreationConfig); + given(autoCreationConfig.enabled()).willReturn(autoOrLazyCreationEnabled); + given(configuration.getConfigData(LazyCreationConfig.class)).willReturn(lazyCreationConfig); + given(lazyCreationConfig.enabled()).willReturn(!autoOrLazyCreationEnabled); + + given(state.createReadableStates(any())).willReturn(readableStates); + + var accountAmounts = new ArrayList(); + accountAmounts.add(AccountAmount.newBuilder() + .amount(-1_000_000_000L) + .accountID(AccountID.newBuilder().accountNum(3333L).build()) + .build()); + accountAmounts.add(AccountAmount.newBuilder() + .amount(+1_000_000_000L) + .accountID(AccountID.newBuilder().accountNum(4444L).build()) + .build()); + final var scheduledTransferNoAliases = SchedulableTransactionBody.newBuilder() + .cryptoTransfer(CryptoTransferTransactionBody.newBuilder() + .transfers(TransferList.newBuilder() + .accountAmounts(accountAmounts) + .build())) + .build(); + + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles.json"); + subject.rebuildFor(defs); + + // when + final var txnInfo = scheduleCreate(scheduledTransferNoAliases, false, null); + final boolean ans = subject.shouldThrottle(txnInfo, CONSENSUS_NOW, state); + final var throttlesNow = subject.activeThrottlesFor(SCHEDULE_CREATE); + final var aNow = throttlesNow.get(0); + + // then + assertFalse(ans); + assertEquals(BucketThrottle.capacityUnitsPerTxn(), aNow.used()); + assertEquals( + longTermEnabled && throttleType == FRONTEND_THROTTLE ? BucketThrottle.capacityUnitsPerTxn() : 0, + subject.activeThrottlesFor(CRYPTO_TRANSFER).get(0).used()); + } + + @ParameterizedTest + @CsvSource({ + "FRONTEND_THROTTLE,true,true", + "FRONTEND_THROTTLE,true,false", + "FRONTEND_THROTTLE,false,true", + "FRONTEND_THROTTLE,false,false", + "BACKEND_THROTTLE,true,true", + "BACKEND_THROTTLE,true,false", + "BACKEND_THROTTLE,false,true", + "BACKEND_THROTTLE,false,false", + }) + void doesntUseCryptoCreateThrottleForNonCryptoTransfer( + final ThrottleAccumulator.ThrottleType throttleType, + final boolean autoCreationEnabled, + final boolean lazyCreationEnabled) + throws IOException { + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, throttleType); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + given(configuration.getConfigData(SchedulingConfig.class)).willReturn(schedulingConfig); + given(schedulingConfig.longTermEnabled()).willReturn(false); + given(configuration.getConfigData(AutoCreationConfig.class)).willReturn(autoCreationConfig); + given(autoCreationConfig.enabled()).willReturn(autoCreationEnabled); + given(configuration.getConfigData(LazyCreationConfig.class)).willReturn(lazyCreationConfig); + given(lazyCreationConfig.enabled()).willReturn(lazyCreationEnabled); + + final var scheduledTxn = SchedulableTransactionBody.newBuilder() + .consensusSubmitMessage(ConsensusSubmitMessageTransactionBody.DEFAULT) + .build(); + + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles.json"); + subject.rebuildFor(defs); + + // when + final var txnInfo = scheduleCreate(scheduledTxn, false, null); + final boolean ans = subject.shouldThrottle(txnInfo, CONSENSUS_NOW, state); + final var throttlesNow = subject.activeThrottlesFor(SCHEDULE_CREATE); + final var aNow = throttlesNow.get(0); + + // then + assertFalse(ans); + assertEquals(BucketThrottle.capacityUnitsPerTxn(), aNow.used()); + } + + @ParameterizedTest + @CsvSource({ + "FRONTEND_THROTTLE,true", + "FRONTEND_THROTTLE,false", + "BACKEND_THROTTLE,true", + "BACKEND_THROTTLE,false", + }) + @MockitoSettings(strictness = org.mockito.quality.Strictness.LENIENT) + void usesCryptoCreateThrottleForCryptoTransferWithAutoCreationInScheduleCreate( + final ThrottleAccumulator.ThrottleType throttleType, final boolean longTermEnabled) throws IOException { + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, throttleType); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + given(configuration.getConfigData(SchedulingConfig.class)).willReturn(schedulingConfig); + given(schedulingConfig.longTermEnabled()).willReturn(longTermEnabled); + given(configuration.getConfigData(AutoCreationConfig.class)).willReturn(autoCreationConfig); + given(autoCreationConfig.enabled()).willReturn(true); + given(configuration.getConfigData(LazyCreationConfig.class)).willReturn(lazyCreationConfig); + given(lazyCreationConfig.enabled()).willReturn(false); + + given(state.createReadableStates(any())).willReturn(readableStates); + given(readableStates.get(any())).willReturn(aliases); + + final var alias = keyToBytes(A_PRIMITIVE_KEY); + if (!(throttleType != FRONTEND_THROTTLE && longTermEnabled)) { + given(aliases.get(any())).willReturn(null); + } + + var accountAmounts = new ArrayList(); + accountAmounts.add(AccountAmount.newBuilder() + .amount(-1_000_000_000L) + .accountID(AccountID.newBuilder().accountNum(3333L).build()) + .build()); + accountAmounts.add(AccountAmount.newBuilder() + .amount(+1_000_000_000L) + .accountID(AccountID.newBuilder().alias(alias).build()) + .build()); + final var scheduledTransferWithAutoCreation = SchedulableTransactionBody.newBuilder() + .cryptoTransfer(CryptoTransferTransactionBody.newBuilder() + .transfers(TransferList.newBuilder() + .accountAmounts(accountAmounts) + .build())) + .build(); + + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles.json"); + subject.rebuildFor(defs); + + // when + final var txnInfo = scheduleCreate(scheduledTransferWithAutoCreation, false, null); + final boolean ans = subject.shouldThrottle(txnInfo, CONSENSUS_NOW, state); + final var throttlesNow = subject.activeThrottlesFor(SCHEDULE_CREATE); + final var aNow = throttlesNow.get(0); + + // then + assertFalse(ans); + if (longTermEnabled && throttleType == FRONTEND_THROTTLE) { + // with long term enabled, we count the schedule create in addition to the auto + // creations, which + // is how it should have been to start with + assertEquals(51 * BucketThrottle.capacityUnitsPerTxn(), aNow.used()); + } else if (longTermEnabled) { + // with long term enabled, consensus throttles do not count the contained txn + assertEquals(BucketThrottle.capacityUnitsPerTxn(), aNow.used()); + } else { + assertEquals(50 * BucketThrottle.capacityUnitsPerTxn(), aNow.used()); + } + + assertEquals(0, subject.activeThrottlesFor(CRYPTO_TRANSFER).get(0).used()); + } + + @ParameterizedTest + @CsvSource({ + "FRONTEND_THROTTLE,true", + "FRONTEND_THROTTLE,false", + "BACKEND_THROTTLE,true", + "BACKEND_THROTTLE,false", + }) + @MockitoSettings(strictness = org.mockito.quality.Strictness.LENIENT) + void usesScheduleCreateThrottleForAliasedCryptoTransferWithNoAutoCreation( + final ThrottleAccumulator.ThrottleType throttleType, final boolean longTermEnabled) throws IOException { + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, throttleType); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + given(configuration.getConfigData(SchedulingConfig.class)).willReturn(schedulingConfig); + given(schedulingConfig.longTermEnabled()).willReturn(longTermEnabled); + given(configuration.getConfigData(AutoCreationConfig.class)).willReturn(autoCreationConfig); + given(autoCreationConfig.enabled()).willReturn(true); + given(configuration.getConfigData(LazyCreationConfig.class)).willReturn(lazyCreationConfig); + given(lazyCreationConfig.enabled()).willReturn(false); + + given(state.createReadableStates(any())).willReturn(readableStates); + given(readableStates.get(any())).willReturn(aliases); + + final var alias = keyToBytes(A_PRIMITIVE_KEY); + if (!(throttleType != FRONTEND_THROTTLE && longTermEnabled)) { + given(aliases.get(any())) + .willReturn(AccountID.newBuilder().accountNum(1_234L).build()); + } + + var accountAmounts = new ArrayList(); + accountAmounts.add(AccountAmount.newBuilder() + .amount(-1_000_000_000L) + .accountID(AccountID.newBuilder().accountNum(3333L).build()) + .build()); + accountAmounts.add(AccountAmount.newBuilder() + .amount(+1_000_000_000L) + .accountID(AccountID.newBuilder().alias(alias).build()) + .build()); + final var scheduledTransferWithAutoCreation = SchedulableTransactionBody.newBuilder() + .cryptoTransfer(CryptoTransferTransactionBody.newBuilder() + .transfers(TransferList.newBuilder() + .accountAmounts(accountAmounts) + .build())) + .build(); + + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles.json"); + subject.rebuildFor(defs); + + // when + final var txnInfo = scheduleCreate(scheduledTransferWithAutoCreation, false, null); + final boolean ans = subject.shouldThrottle(txnInfo, CONSENSUS_NOW, state); + final var throttlesNow = subject.activeThrottlesFor(SCHEDULE_CREATE); + final var aNow = throttlesNow.get(0); + + // then + assertFalse(ans); + assertEquals(BucketThrottle.capacityUnitsPerTxn(), aNow.used()); + + assertEquals( + longTermEnabled && throttleType == FRONTEND_THROTTLE ? BucketThrottle.capacityUnitsPerTxn() : 0, + subject.activeThrottlesFor(CRYPTO_TRANSFER).get(0).used()); + } + + @Test + void reclaimsAllUsagesOnThrottledShouldThrottleTxn() throws IOException { + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, FRONTEND_THROTTLE); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + given(configuration.getConfigData(SchedulingConfig.class)).willReturn(schedulingConfig); + given(schedulingConfig.longTermEnabled()).willReturn(true); + + final var scheduledSubmit = SchedulableTransactionBody.newBuilder() + .consensusSubmitMessage(ConsensusSubmitMessageTransactionBody.DEFAULT) + .build(); + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles-inverted.json"); + subject.rebuildFor(defs); + + final var txnInfo = scheduleCreate(scheduledSubmit, false, null); + final boolean firstAns = subject.shouldThrottle(txnInfo, CONSENSUS_NOW, state); + boolean subsequentAns = false; + for (int i = 1; i <= 150; i++) { + subsequentAns = subject.shouldThrottle(txnInfo, CONSENSUS_NOW.plusNanos(i), state); + } + + assertFalse(firstAns); + assertTrue(subsequentAns); + assertEquals( + 4999250000000L, + subject.activeThrottlesFor(SCHEDULE_CREATE).get(0).used()); + + assertEquals( + 4999999250000L, + subject.activeThrottlesFor(CONSENSUS_SUBMIT_MESSAGE).get(0).used()); + + // when + subject.resetUsage(); + + // then + assertEquals(0L, subject.activeThrottlesFor(SCHEDULE_CREATE).get(0).used()); + assertEquals( + 0L, subject.activeThrottlesFor(CONSENSUS_SUBMIT_MESSAGE).get(0).used()); + } + + @ParameterizedTest + @CsvSource({ + "FRONTEND_THROTTLE,true,true", + "FRONTEND_THROTTLE,true,false", + "FRONTEND_THROTTLE,false,true", + "FRONTEND_THROTTLE,false,false", + "BACKEND_THROTTLE,true,true", + "BACKEND_THROTTLE,true,false", + "BACKEND_THROTTLE,false,true", + "BACKEND_THROTTLE,false,false", + }) + void usesScheduleSignThrottle( + final ThrottleAccumulator.ThrottleType throttleType, + final boolean longTermEnabled, + final boolean waitForExpiry) + throws IOException { + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, throttleType); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + given(configuration.getConfigData(SchedulingConfig.class)).willReturn(schedulingConfig); + given(schedulingConfig.longTermEnabled()).willReturn(longTermEnabled); + + if (longTermEnabled && throttleType == FRONTEND_THROTTLE) { + final var scheduledSubmit = SchedulableTransactionBody.newBuilder() + .consensusSubmitMessage(ConsensusSubmitMessageTransactionBody.DEFAULT) + .build(); + + final var txnInfo = scheduleCreate(scheduledSubmit, waitForExpiry, null); + final var schedule = Schedule.newBuilder() + .waitForExpiry(txnInfo.txBody().scheduleCreate().waitForExpiry()) + .originalCreateTransaction(txnInfo.txBody()) + .payerAccountId(txnInfo.payerID()) + .scheduledTransaction(scheduledSubmit) + .build(); + + given(state.createReadableStates(any())).willReturn(readableStates); + given(readableStates.get(any())).willReturn(schedules); + given(schedules.get(SCHEDULE_ID)).willReturn(schedule); + } + + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles.json"); + subject.rebuildFor(defs); + + // when + final var txnInfo = scheduleSign(SCHEDULE_ID); + final boolean firstAns = subject.shouldThrottle(txnInfo, CONSENSUS_NOW, state); + boolean subsequentAns = false; + for (int i = 1; i <= 150; i++) { + subsequentAns = subject.shouldThrottle(txnInfo, CONSENSUS_NOW.plusNanos(i), state); + } + + final var throttlesNow = subject.activeThrottlesFor(SCHEDULE_SIGN); + final var aNow = throttlesNow.get(0); + + // then + assertFalse(firstAns); + assertTrue(subsequentAns); + assertEquals(149999992500000L, aNow.used()); + + assertEquals( + longTermEnabled && throttleType == FRONTEND_THROTTLE && (!waitForExpiry) ? 149999255000000L : 0, + subject.activeThrottlesFor(CONSENSUS_SUBMIT_MESSAGE).get(0).used()); + } + + @ParameterizedTest + @CsvSource({ + "FRONTEND_THROTTLE,true,true", + "FRONTEND_THROTTLE,true,false", + "FRONTEND_THROTTLE,false,true", + "FRONTEND_THROTTLE,false,false", + "BACKEND_THROTTLE,true,true", + "BACKEND_THROTTLE,true,false", + "BACKEND_THROTTLE,false,true", + "BACKEND_THROTTLE,false,false", + }) + void usesScheduleSignThrottleWithNestedThrottleExempt( + final ThrottleAccumulator.ThrottleType throttleType, + final boolean longTermEnabled, + final boolean waitForExpiry) + throws IOException { + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, throttleType); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + given(configuration.getConfigData(SchedulingConfig.class)).willReturn(schedulingConfig); + given(schedulingConfig.longTermEnabled()).willReturn(longTermEnabled); + + if (longTermEnabled && throttleType == FRONTEND_THROTTLE) { + final var scheduledSubmit = SchedulableTransactionBody.newBuilder() + .consensusSubmitMessage(ConsensusSubmitMessageTransactionBody.DEFAULT) + .build(); + + final var txnInfo = scheduleCreate( + scheduledSubmit, + waitForExpiry, + AccountID.newBuilder().accountNum(2L).build()); + final var schedule = Schedule.newBuilder() + .waitForExpiry(txnInfo.txBody().scheduleCreate().waitForExpiry()) + .originalCreateTransaction(txnInfo.txBody()) + .payerAccountId(AccountID.newBuilder().accountNum(2L).build()) + .scheduledTransaction(scheduledSubmit) + .build(); + + given(state.createReadableStates(any())).willReturn(readableStates); + given(readableStates.get(any())).willReturn(schedules); + given(schedules.get(SCHEDULE_ID)).willReturn(schedule); + } + + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles.json"); + subject.rebuildFor(defs); + + // when + final var txnInfo = scheduleSign(SCHEDULE_ID); + final boolean firstAns = subject.shouldThrottle(txnInfo, CONSENSUS_NOW, state); + boolean subsequentAns = false; + for (int i = 1; i <= 150; i++) { + subsequentAns = subject.shouldThrottle(txnInfo, CONSENSUS_NOW.plusNanos(i), state); + } + + final var throttlesNow = subject.activeThrottlesFor(SCHEDULE_SIGN); + final var aNow = throttlesNow.get(0); + + // then + assertFalse(firstAns); + assertTrue(subsequentAns); + assertEquals(149999992500000L, aNow.used()); + + assertEquals( + 0, subject.activeThrottlesFor(CONSENSUS_SUBMIT_MESSAGE).get(0).used()); + } + + @Test + void scheduleSignAlwaysThrottledWhenNoBody() throws IOException { + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, FRONTEND_THROTTLE); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + given(configuration.getConfigData(SchedulingConfig.class)).willReturn(schedulingConfig); + given(schedulingConfig.longTermEnabled()).willReturn(true); + + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles.json"); + subject.rebuildFor(defs); + + final var scheduleCreateTxnInfo = scheduleCreate(SchedulableTransactionBody.DEFAULT, false, null); + final var schedule = Schedule.newBuilder() + .waitForExpiry(scheduleCreateTxnInfo.txBody().scheduleCreate().waitForExpiry()) + .originalCreateTransaction(scheduleCreateTxnInfo.txBody()) + .payerAccountId(AccountID.newBuilder().accountNum(2L).build()) + .scheduledTransaction(SchedulableTransactionBody.DEFAULT) + .build(); + + given(state.createReadableStates(any())).willReturn(readableStates); + given(readableStates.get(any())).willReturn(schedules); + given(schedules.get(SCHEDULE_ID)).willReturn(schedule); + + // when + final var scheduleSignTxnInfo = scheduleSign(SCHEDULE_ID); + final var firstAns = subject.shouldThrottle(scheduleSignTxnInfo, CONSENSUS_NOW, state); + for (int i = 1; i <= 150; i++) { + assertTrue(subject.shouldThrottle(scheduleSignTxnInfo, CONSENSUS_NOW.plusNanos(i), state)); + } + + final var throttlesNow = subject.activeThrottlesFor(SCHEDULE_SIGN); + final var aNow = throttlesNow.get(0); + + // then + assertTrue(firstAns); + assertEquals(0L, aNow.used()); + assertEquals( + 0, subject.activeThrottlesFor(CONSENSUS_SUBMIT_MESSAGE).get(0).used()); + } + + @Test + void scheduleSignAlwaysThrottledWhenNotExisting() throws IOException { + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, FRONTEND_THROTTLE); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + given(configuration.getConfigData(SchedulingConfig.class)).willReturn(schedulingConfig); + given(schedulingConfig.longTermEnabled()).willReturn(true); + + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles.json"); + subject.rebuildFor(defs); + + given(state.createReadableStates(any())).willReturn(readableStates); + given(readableStates.get(any())).willReturn(schedules); + + // when + final var scheduleSignTxnInfo = scheduleSign(SCHEDULE_ID); + final var firstAns = subject.shouldThrottle(scheduleSignTxnInfo, CONSENSUS_NOW, state); + for (int i = 1; i <= 150; i++) { + assertTrue(subject.shouldThrottle(scheduleSignTxnInfo, CONSENSUS_NOW.plusNanos(i), state)); + } + + final var throttlesNow = subject.activeThrottlesFor(SCHEDULE_SIGN); + final var aNow = throttlesNow.get(0); + + assertTrue(firstAns); + assertEquals(0L, aNow.used()); + + assertEquals( + 0, subject.activeThrottlesFor(CONSENSUS_SUBMIT_MESSAGE).get(0).used()); + } + + @ParameterizedTest + @CsvSource({ + "FRONTEND_THROTTLE,true", + "FRONTEND_THROTTLE,false", + "BACKEND_THROTTLE,true", + "BACKEND_THROTTLE,false", + }) + @MockitoSettings(strictness = org.mockito.quality.Strictness.LENIENT) + void usesCryptoCreateThrottleForCryptoTransferWithAutoCreationInScheduleSign( + final ThrottleAccumulator.ThrottleType throttleType, final boolean longTermEnabled) throws IOException { + + // given + subject = new ThrottleAccumulator(() -> CAPACITY_SPLIT, configProvider, throttleType); + + given(configProvider.getConfiguration()).willReturn(configuration); + given(configuration.getConfigData(AccountsConfig.class)).willReturn(accountsConfig); + given(accountsConfig.lastThrottleExempt()).willReturn(100L); + given(configuration.getConfigData(ContractsConfig.class)).willReturn(contractsConfig); + given(contractsConfig.throttleThrottleByGas()).willReturn(false); + given(configuration.getConfigData(SchedulingConfig.class)).willReturn(schedulingConfig); + given(schedulingConfig.longTermEnabled()).willReturn(longTermEnabled); + given(configuration.getConfigData(AutoCreationConfig.class)).willReturn(autoCreationConfig); + given(autoCreationConfig.enabled()).willReturn(true); + given(configuration.getConfigData(LazyCreationConfig.class)).willReturn(lazyCreationConfig); + given(lazyCreationConfig.enabled()).willReturn(false); + + given(state.createReadableStates(any())).willReturn(readableStates); + given(readableStates.get("ALIASES")).willReturn(aliases); + + final var alias = keyToBytes(A_PRIMITIVE_KEY); + if (throttleType == FRONTEND_THROTTLE && longTermEnabled) { + given(aliases.get(any())).willReturn(null); + } + + if (longTermEnabled && throttleType == FRONTEND_THROTTLE) { + var accountAmounts = new ArrayList(); + accountAmounts.add(AccountAmount.newBuilder() + .amount(-1_000_000_000L) + .accountID(AccountID.newBuilder().accountNum(3333L).build()) + .build()); + accountAmounts.add(AccountAmount.newBuilder() + .amount(+1_000_000_000L) + .accountID(AccountID.newBuilder().alias(alias).build()) + .build()); + final var scheduledTransferWithAutoCreation = SchedulableTransactionBody.newBuilder() + .cryptoTransfer(CryptoTransferTransactionBody.newBuilder() + .transfers(TransferList.newBuilder() + .accountAmounts(accountAmounts) + .build())) + .build(); + + final var scheduleCreateTxnInfo = scheduleCreate(scheduledTransferWithAutoCreation, false, null); + final var schedule = Schedule.newBuilder() + .waitForExpiry( + scheduleCreateTxnInfo.txBody().scheduleCreate().waitForExpiry()) + .originalCreateTransaction(scheduleCreateTxnInfo.txBody()) + .payerAccountId(scheduleCreateTxnInfo.payerID()) + .scheduledTransaction(scheduledTransferWithAutoCreation) + .build(); + given(readableStates.get(SCHEDULES_BY_ID_KEY)).willReturn(schedules); + given(schedules.get(SCHEDULE_ID)).willReturn(schedule); + } + + final var defs = getThrottleDefs("bootstrap/schedule-create-throttles.json"); + subject.rebuildFor(defs); + + // when + final var scheduleSignTxnInfo = scheduleSign(SCHEDULE_ID); + final var ans = subject.shouldThrottle(scheduleSignTxnInfo, CONSENSUS_NOW, state); + final var throttlesNow = subject.activeThrottlesFor(SCHEDULE_SIGN); + final var aNow = throttlesNow.get(0); + + // then + assertFalse(ans); + if (longTermEnabled && throttleType == FRONTEND_THROTTLE) { + // with long term enabled, we count the schedule create in addition to the auto + // creations, which + // is how it should have been to start with + assertEquals(51 * BucketThrottle.capacityUnitsPerTxn(), aNow.used()); + } else { + // with long term disabled or mode not being HAPI, ScheduleSign is the only part that + // counts + assertEquals(BucketThrottle.capacityUnitsPerTxn(), aNow.used()); + } + + assertEquals(0, subject.activeThrottlesFor(CRYPTO_TRANSFER).get(0).used()); + } + + @NotNull + private static Bytes keyToBytes(Key key) throws IOException { + final var dataBuffer = getThreadLocalDataBuffer(); + Key.PROTOBUF.write(key, dataBuffer); + // clamp limit to bytes written + dataBuffer.limit(dataBuffer.position()); + return dataBuffer.getBytes(0, dataBuffer.length()); + } + + private TransactionInfo scheduleCreate( + final SchedulableTransactionBody inner, boolean waitForExpiry, AccountID customPayer) { + final var schedule = ScheduleCreateTransactionBody.newBuilder() + .waitForExpiry(waitForExpiry) + .scheduledTransactionBody(inner); + if (customPayer != null) { + schedule.payerAccountID(customPayer); + } + final var body = TransactionBody.newBuilder() + .transactionID(TransactionID.newBuilder().accountID(PAYER_ID).build()) + .scheduleCreate(schedule) + .build(); + final var txn = Transaction.newBuilder().body(body).build(); + return new TransactionInfo( + txn, + body, + TransactionID.newBuilder().accountID(PAYER_ID).build(), + PAYER_ID, + SignatureMap.DEFAULT, + Bytes.EMPTY, + SCHEDULE_CREATE); + } + + private TransactionInfo scheduleSign(ScheduleID scheduleID) { + final var schedule = ScheduleSignTransactionBody.newBuilder().scheduleID(scheduleID); + final var body = TransactionBody.newBuilder() + .transactionID(TransactionID.newBuilder().accountID(PAYER_ID).build()) + .scheduleSign(schedule) + .build(); + final var txn = Transaction.newBuilder().body(body).build(); + return new TransactionInfo( + txn, + body, + TransactionID.newBuilder().accountID(PAYER_ID).build(), + PAYER_ID, + SignatureMap.DEFAULT, + Bytes.EMPTY, + SCHEDULE_SIGN); + } + + private ThrottleDefinitions getThrottleDefs(String testResource) throws IOException { + try (InputStream in = ThrottleDefinitions.class.getClassLoader().getResourceAsStream(testResource)) { + var om = new ObjectMapper(); + var throttleDefinitionsObj = om.readValue( + in, com.hedera.node.app.hapi.utils.sysfiles.domain.throttling.ThrottleDefinitions.class); + final var throttleDefsBytes = + Bytes.wrap(throttleDefinitionsObj.toProto().toByteArray()); + return ThrottleDefinitions.PROTOBUF.parse(throttleDefsBytes.toReadableSequentialData()); + } + } + + private CryptoTransferTransactionBody cryptoTransferWithImplicitCreations(int numImplicitCreations) { + var accountAmounts = new ArrayList(); + for (int i = 1; i <= numImplicitCreations; i++) { + accountAmounts.add(AccountAmount.newBuilder() + .accountID(AccountID.newBuilder() + .alias(Bytes.wrap("abcdeabcdeabcdeabcde")) + .build()) + .amount(i) + .build()); + } + + return CryptoTransferTransactionBody.newBuilder() + .transfers( + TransferList.newBuilder().accountAmounts(accountAmounts).build()) + .build(); + } +} diff --git a/hedera-node/hedera-app/src/test/resources/bootstrap/schedule-create-throttles-inverted.json b/hedera-node/hedera-app/src/test/resources/bootstrap/schedule-create-throttles-inverted.json new file mode 100644 index 000000000000..7327ede332aa --- /dev/null +++ b/hedera-node/hedera-app/src/test/resources/bootstrap/schedule-create-throttles-inverted.json @@ -0,0 +1,42 @@ +{ + "buckets": [ + { + "name": "A", + "burstPeriod": 1, + "throttleGroups": [ + { + "operations": [ + "ContractCallLocal" + ], + "opsPerSec": 1 + } + ] + }, + { + "name": "A", + "burstPeriod": 1, + "throttleGroups": [ + { + "operations": [ + "CryptoTransfer", + "ConsensusSubmitMessage" + ], + "opsPerSec": 10 + } + ] + }, + { + "name": "C", + "burstPeriod": 1, + "throttleGroups": [ + { + "operations": [ + "ScheduleCreate", + "ScheduleSign" + ], + "opsPerSec": 10000 + } + ] + } + ] +} diff --git a/hedera-node/hedera-app/src/test/resources/bootstrap/schedule-create-throttles.json b/hedera-node/hedera-app/src/test/resources/bootstrap/schedule-create-throttles.json new file mode 100644 index 000000000000..4e40aefda917 --- /dev/null +++ b/hedera-node/hedera-app/src/test/resources/bootstrap/schedule-create-throttles.json @@ -0,0 +1,36 @@ +{ + "buckets": [ + { + "name": "A", + "burstPeriod": 2, + "throttleGroups": [ + { + "operations": [ + "CryptoTransfer", + "ConsensusSubmitMessage" + ], + "opsPerSec": 10000 + } + ] + }, + { + "name": "C", + "burstPeriod": 3, + "throttleGroups": [ + { + "operations": [ + "CryptoCreate" + ], + "opsPerSec": 2 + }, + { + "operations": [ + "ScheduleCreate", + "ScheduleSign" + ], + "opsPerSec": 100 + } + ] + } + ] +} diff --git a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/HandlerUtility.java b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/HandlerUtility.java index fbc4101c3cc5..c47db26ae06a 100644 --- a/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/HandlerUtility.java +++ b/hedera-node/hedera-schedule-service-impl/src/main/java/com/hedera/node/app/service/schedule/impl/handlers/HandlerUtility.java @@ -40,11 +40,11 @@ /** * A package-private utility class for Schedule Handlers. */ -final class HandlerUtility { +public final class HandlerUtility { private HandlerUtility() {} @NonNull - static TransactionBody childAsOrdinary(@NonNull final Schedule scheduleInState) { + public static TransactionBody childAsOrdinary(@NonNull final Schedule scheduleInState) { final TransactionID scheduledTransactionId = transactionIdForScheduled(scheduleInState); final SchedulableTransactionBody scheduledTransaction = scheduleInState.scheduledTransaction(); final TransactionBody.Builder ordinary = TransactionBody.newBuilder();