Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce gasUnit property to allow greater gas per second throttle #9994

Merged
merged 2 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 53 additions & 52 deletions docs/configuration.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.hedera.mirror.web3.exception.RateLimitException;
import com.hedera.mirror.web3.service.ContractExecutionService;
import com.hedera.mirror.web3.service.model.ContractExecutionParameters;
import com.hedera.mirror.web3.throttle.ThrottleProperties;
import com.hedera.mirror.web3.viewmodel.ContractCallRequest;
import com.hedera.mirror.web3.viewmodel.ContractCallResponse;
import com.hedera.node.app.service.evm.store.models.HederaEvmAccount;
Expand Down Expand Up @@ -57,12 +58,14 @@ class ContractController {

private final MirrorNodeEvmProperties evmProperties;

private final ThrottleProperties throttleProperties;

@PostMapping(value = "/call")
ContractCallResponse call(@RequestBody @Valid ContractCallRequest request) {

if (!rateLimitBucket.tryConsume(1)) {
throw new RateLimitException("Requests per second rate limit exceeded.");
} else if (!gasLimitBucket.tryConsume(request.getGas())) {
} else if (!gasLimitBucket.tryConsume(Math.floorDiv(request.getGas(), throttleProperties.getGasUnit()))) {
throw new RateLimitException("Gas per second rate limit exceeded.");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,18 @@ protected HederaEvmTransactionProcessingResult doProcessCall(
}

private void restoreGasToBucket(HederaEvmTransactionProcessingResult result, long gasLimit) {
final var gasUnit = throttleProperties.getGasUnit();
// If the transaction fails, gasUsed is equal to gasLimit, so restore the configured refund percent
// of the gasLimit value back in the bucket.
final var gasLimitToRestoreBaseline = (long) (gasLimit * throttleProperties.getGasLimitRefundPercent() / 100f);
final var gasLimitToRestoreBaseline =
(long) (Math.floorDiv(gasLimit, gasUnit) * throttleProperties.getGasLimitRefundPercent() / 100f);
if (!result.isSuccessful() && gasLimit == result.getGasUsed()) {
gasLimitBucket.addTokens(gasLimitToRestoreBaseline);
} else {
// The transaction was successful or reverted, so restore the remaining gas back in the bucket or
// the configured refund percent of the gasLimit value back in the bucket - whichever is lower.
gasLimitBucket.addTokens(Math.min(gasLimit - result.getGasUsed(), gasLimitToRestoreBaseline));
final var gasRemaining = gasLimit - result.getGasUsed();
gasLimitBucket.addTokens(Math.min(Math.floorDiv(gasRemaining, gasUnit), gasLimitToRestoreBaseline));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@ public class ThrottleProperties {

@Getter
@Min(21_000)
@Max(1_000_000_000)
private long gasPerSecond = 1_000_000_000L;

@Getter
@Min(1)
steven-sheehy marked this conversation as resolved.
Show resolved Hide resolved
private int gasUnit = 1;

@Getter
@Min(0)
@Max(100)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
Expand All @@ -42,6 +43,7 @@
import com.hedera.mirror.web3.exception.InvalidParametersException;
import com.hedera.mirror.web3.exception.MirrorEvmTransactionException;
import com.hedera.mirror.web3.service.ContractExecutionService;
import com.hedera.mirror.web3.throttle.ThrottleProperties;
import com.hedera.mirror.web3.viewmodel.BlockType;
import com.hedera.mirror.web3.viewmodel.ContractCallRequest;
import com.hedera.mirror.web3.viewmodel.GenericErrorResponse;
Expand Down Expand Up @@ -108,10 +110,15 @@ class ContractControllerTest {
@Autowired
private MirrorNodeEvmProperties evmProperties;

@MockBean
private ThrottleProperties throttleProperties;

@BeforeEach
void setUp() {
given(rateLimitBucket.tryConsume(1)).willReturn(true);
given(gasLimitBucket.tryConsume(THROTTLE_GAS_LIMIT)).willReturn(true);
given(throttleProperties.getGasUnit()).willReturn(2);
given(gasLimitBucket.tryConsume(Math.floorDiv(THROTTLE_GAS_LIMIT, throttleProperties.getGasUnit())))
.willReturn(true);
}

@SneakyThrows
Expand Down Expand Up @@ -167,6 +174,7 @@ void estimateGasWithInvalidGasParameter(long gas) throws Exception {
? numberErrorString("gas", "greater", 21000L)
: numberErrorString("gas", "less", 15_000_000L);
given(gasLimitBucket.tryConsume(gas)).willReturn(true);
given(throttleProperties.getGasUnit()).willReturn(1);
final var request = request();
request.setEstimate(true);
request.setGas(gas);
Expand All @@ -188,16 +196,25 @@ void exceedingRateLimit() throws Exception {

@Test
void exceedingGasLimit() throws Exception {
given(gasLimitBucket.tryConsume(THROTTLE_GAS_LIMIT)).willReturn(false);
given(gasLimitBucket.tryConsume(anyLong())).willReturn(false);
contractCall(request()).andExpect(status().isTooManyRequests());
}

@Test
void throttleGasScaledOnConsume() throws Exception {
var request = request();
request.setGas(200_000L);
given(gasLimitBucket.tryConsume(anyLong())).willReturn(true);
contractCall(request).andExpect(status().isOk());
verify(gasLimitBucket).tryConsume(100_000L);
}

@Test
void restoreGasInThrottleBucketOnValidationFail() throws Exception {
var request = request();
request.setData("With invalid symbol!");
contractCall(request).andExpect(status().isBadRequest());
verify(gasLimitBucket).tryConsume(request.getGas());
verify(gasLimitBucket).tryConsume(Math.floorDiv(request.getGas(), throttleProperties.getGasUnit()));
verify(gasLimitBucket).addTokens(request.getGas());
}

Expand Down Expand Up @@ -566,5 +583,10 @@ MeterRegistry meterRegistry() {
Web3Properties web3Properties() {
return new Web3Properties();
}

@Bean
ThrottleProperties throttleProperties() {
return new ThrottleProperties();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.params.provider.EnumSource.Mode.INCLUDE;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

Expand Down Expand Up @@ -73,12 +73,12 @@
import java.util.stream.Stream;
import org.apache.tuweni.bytes.Bytes;
import org.hyperledger.besu.datatypes.Address;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -103,7 +103,7 @@ class ContractCallServiceTest extends AbstractContractCallServiceTest {
@Autowired
private RecordFileService recordFileService;

@Autowired
@Mock
private ThrottleProperties throttleProperties;

@Autowired
Expand Down Expand Up @@ -132,10 +132,12 @@ private static Stream<Arguments> provideCustomBlockTypes() {

private static Stream<Arguments> ercPrecompileCallTypeArgumentsProvider() {
List<Long> gasLimits = List.of(15_000_000L, 30_000L);
List<Integer> gasUnits = List.of(1, 2);

return Arrays.stream(CallType.values())
.filter(callType -> !callType.equals(ERROR))
.flatMap(callType -> gasLimits.stream().map(gasLimit -> Arguments.of(callType, gasLimit)));
.flatMap(callType -> gasLimits.stream().flatMap(gasLimit -> gasUnits.stream()
.map(gasUnit -> Arguments.of(callType, gasLimit, gasUnit))));
}

private static String toHexWith64LeadingZeros(final Long value) {
Expand All @@ -145,6 +147,14 @@ private static String toHexWith64LeadingZeros(final Long value) {
return result;
}

@Override
@BeforeEach
protected void setup() {
super.setup();
given(throttleProperties.getGasLimitRefundPercent()).willReturn(100f);
given(throttleProperties.getGasUnit()).willReturn(1);
}

@Test
void callWithoutDataToAddressWithNoBytecodeReturnsEmptyResult() {
// Given
Expand Down Expand Up @@ -726,11 +736,8 @@ void stateChangeWorksWithDynamicEthCall() throws Exception {
}

@ParameterizedTest
@EnumSource(
value = CallType.class,
names = {"ETH_CALL", "ETH_ESTIMATE_GAS"},
mode = INCLUDE)
void ercPrecompileExceptionalHaltReturnsExpectedGasToBucket(final CallType callType) {
@MethodSource("provideParametersForErcPrecompileExceptionalHalt")
void ercPrecompileExceptionalHaltReturnsExpectedGasToBucket(final CallType callType, final int gasUnit) {
// Given
final var token = tokenPersist();
final var contract = testWeb3jService.deploy(ERCTestContract::deploy);
Expand All @@ -739,8 +746,12 @@ void ercPrecompileExceptionalHaltReturnsExpectedGasToBucket(final CallType callT

final var serviceParameters = getContractExecutionParametersWithValue(
Bytes.fromHexString(functionCall.encodeFunctionCall()), Address.ZERO, Address.ZERO, callType, 100L);
final var expectedUsedGasByThrottle =
(long) (serviceParameters.getGas() * throttleProperties.getGasLimitRefundPercent() / 100f);

given(throttleProperties.getGasUnit()).willReturn(gasUnit);

final long expectedUsedGasByThrottle = (long)
(Math.floorDiv(TRANSACTION_GAS_LIMIT, gasUnit) * throttleProperties.getGasLimitRefundPercent() / 100f);

final var contractCallServiceWithMockedGasLimitBucket = new ContractExecutionService(
meterRegistry,
binaryGasEstimator,
Expand All @@ -766,16 +777,19 @@ void ercPrecompileExceptionalHaltReturnsExpectedGasToBucket(final CallType callT

@ParameterizedTest
@MethodSource("ercPrecompileCallTypeArgumentsProvider")
void ercPrecompileContractRevertReturnsExpectedGasToBucket(final CallType callType, final long gasLimit) {
void ercPrecompileContractRevertReturnsExpectedGasToBucket(
final CallType callType, final long gasLimit, final int gasUnit) {
// Given
final var contract = testWeb3jService.deploy(ERCTestContract::deploy);
final var functionCall = contract.call_nameNonStatic(Address.ZERO.toHexString());
given(throttleProperties.getGasUnit()).willReturn(gasUnit);

final var serviceParameters = getContractExecutionParameters(functionCall, contract, callType, gasLimit);
final var expectedGasUsed = gasUsedAfterExecution(serviceParameters);
final var gasLimitToRestoreBaseline =
(long) (serviceParameters.getGas() * throttleProperties.getGasLimitRefundPercent() / 100f);
final var expectedUsedGasByThrottle = Math.min(gasLimit - expectedGasUsed, gasLimitToRestoreBaseline);
(long) (Math.floorDiv(gasLimit, gasUnit) * throttleProperties.getGasLimitRefundPercent() / 100f);
final var expectedUsedGasByThrottle =
Math.min(Math.floorDiv(gasLimit - expectedGasUsed, gasUnit), gasLimitToRestoreBaseline);
final var contractCallServiceWithMockedGasLimitBucket = new ContractExecutionService(
meterRegistry,
binaryGasEstimator,
Expand All @@ -801,17 +815,20 @@ void ercPrecompileContractRevertReturnsExpectedGasToBucket(final CallType callTy

@ParameterizedTest
@MethodSource("ercPrecompileCallTypeArgumentsProvider")
void ercPrecompileSuccessReturnsExpectedGasToBucket(final CallType callType, final long gasLimit) {
void ercPrecompileSuccessReturnsExpectedGasToBucket(
final CallType callType, final long gasLimit, final int gasUnit) {
// Given
final var token = tokenPersist();
final var contract = testWeb3jService.deploy(ERCTestContract::deploy);
final var functionCall = contract.call_name(toAddress(token.getId()).toHexString());
given(throttleProperties.getGasUnit()).willReturn(gasUnit);

final var serviceParameters = getContractExecutionParameters(functionCall, contract, callType, gasLimit);
final var expectedGasUsed = gasUsedAfterExecution(serviceParameters);
final var gasLimitToRestoreBaseline =
(long) (serviceParameters.getGas() * throttleProperties.getGasLimitRefundPercent() / 100f);
final var expectedUsedGasByThrottle = Math.min(gasLimit - expectedGasUsed, gasLimitToRestoreBaseline);
(long) (Math.floorDiv(gasLimit, gasUnit) * throttleProperties.getGasLimitRefundPercent() / 100f);
final var expectedUsedGasByThrottle =
Math.min(Math.floorDiv(gasLimit - expectedGasUsed, gasUnit), gasLimitToRestoreBaseline);
final var contractCallServiceWithMockedGasLimitBucket = new ContractExecutionService(
meterRegistry,
binaryGasEstimator,
Expand Down Expand Up @@ -955,6 +972,10 @@ private ContractExecutionParameters getContractExecutionParametersWithValue(
.build();
}

private static Stream<Arguments> provideParametersForErcPrecompileExceptionalHalt() {
return Stream.of(Arguments.of(CallType.ETH_CALL, 1), Arguments.of(CallType.ETH_ESTIMATE_GAS, 2));
}

private Entity accountPersist() {
return domainBuilder.entity().persist();
}
Expand Down