diff --git a/rskj-core/src/main/java/co/rsk/RskContext.java b/rskj-core/src/main/java/co/rsk/RskContext.java index cb9d175dac9..aaf85ce6f3b 100644 --- a/rskj-core/src/main/java/co/rsk/RskContext.java +++ b/rskj-core/src/main/java/co/rsk/RskContext.java @@ -21,6 +21,7 @@ import co.rsk.cli.CliArgs; import co.rsk.cli.RskCli; import co.rsk.config.*; +import co.rsk.config.mining.StableMinGasPriceSystemConfig; import co.rsk.core.*; import co.rsk.core.bc.*; import co.rsk.crypto.Keccak256; @@ -40,6 +41,8 @@ import co.rsk.metrics.HashRateCalculatorMining; import co.rsk.metrics.HashRateCalculatorNonMining; import co.rsk.mine.*; +import co.rsk.mine.gas.provider.MinGasPriceProvider; +import co.rsk.mine.gas.provider.MinGasPriceProviderFactory; import co.rsk.net.*; import co.rsk.net.discovery.KnownPeersHandler; import co.rsk.net.discovery.PeerExplorer; @@ -253,6 +256,7 @@ public class RskContext implements NodeContext, NodeBootstrapper { private TxQuotaChecker txQuotaChecker; private GasPriceTracker gasPriceTracker; private BlockChainFlusher blockChainFlusher; + private MinGasPriceProvider minGasPriceProvider; private final Map dbPathToDbKindMap = new HashMap<>(); private volatile boolean closed; @@ -1842,7 +1846,7 @@ private BlockToMineBuilder getBlockToMineBuilder() { getMinerClock(), getBlockFactory(), getBlockExecutor(), - new MinimumGasPriceCalculator(Coin.valueOf(getMiningConfig().getMinGasPriceTarget())), + new MinimumGasPriceCalculator(getMinGasPriceProvider()), new MinerUtils(), getBlockTxSignatureCache() ); @@ -1851,6 +1855,16 @@ private BlockToMineBuilder getBlockToMineBuilder() { return blockToMineBuilder; } + private MinGasPriceProvider getMinGasPriceProvider() { + if (minGasPriceProvider == null) { + long minGasPrice = getRskSystemProperties().minerMinGasPrice(); + StableMinGasPriceSystemConfig stableGasPriceSystemConfig = getRskSystemProperties().getStableGasPriceSystemConfig(); + minGasPriceProvider = MinGasPriceProviderFactory.create(minGasPrice, stableGasPriceSystemConfig, this::getEthModule); + } + logger.debug("MinGasPriceProvider type: {}", minGasPriceProvider.getType().name()); + return minGasPriceProvider; + } + private BlockNodeInformation getBlockNodeInformation() { if (blockNodeInformation == null) { blockNodeInformation = new BlockNodeInformation(); @@ -2142,7 +2156,6 @@ private MiningConfig getMiningConfig() { rskSystemProperties.coinbaseAddress(), rskSystemProperties.minerMinFeesNotifyInDollars(), rskSystemProperties.minerGasUnitInDollars(), - rskSystemProperties.minerMinGasPrice(), rskSystemProperties.getNetworkConstants().getUncleListLimit(), rskSystemProperties.getNetworkConstants().getUncleGenerationLimit(), new GasLimitConfig( @@ -2151,8 +2164,7 @@ private MiningConfig getMiningConfig() { rskSystemProperties.getForceTargetGasLimit() ), rskSystemProperties.isMinerServerFixedClock(), - rskSystemProperties.workSubmissionRateLimitInMills() - ); + rskSystemProperties.workSubmissionRateLimitInMills()); } return miningConfig; @@ -2207,4 +2219,4 @@ private void checkIfNotClosed() { throw new IllegalStateException("RSK Context is closed and cannot be in use anymore"); } } -} \ No newline at end of file +} diff --git a/rskj-core/src/main/java/co/rsk/config/MiningConfig.java b/rskj-core/src/main/java/co/rsk/config/MiningConfig.java index cc536c880b3..bbdf30b8a96 100644 --- a/rskj-core/src/main/java/co/rsk/config/MiningConfig.java +++ b/rskj-core/src/main/java/co/rsk/config/MiningConfig.java @@ -29,7 +29,6 @@ public class MiningConfig { private final RskAddress coinbaseAddress; private final double minFeesNotifyInDollars; private final double minerGasUnitInDollars; - private final long minGasPriceTarget; private final int uncleListLimit; private final int uncleGenerationLimit; private final GasLimitConfig gasLimit; @@ -37,12 +36,11 @@ public class MiningConfig { private final long workSubmissionRateLimitInMills; public MiningConfig(RskAddress coinbaseAddress, double minFeesNotifyInDollars, double minerGasUnitInDollars, - long minGasPriceTarget, int uncleListLimit, int uncleGenerationLimit, GasLimitConfig gasLimit, + int uncleListLimit, int uncleGenerationLimit, GasLimitConfig gasLimit, boolean isFixedClock, long workSubmissionRateLimitInMills) { this.coinbaseAddress = coinbaseAddress; this.minFeesNotifyInDollars = minFeesNotifyInDollars; this.minerGasUnitInDollars = minerGasUnitInDollars; - this.minGasPriceTarget= minGasPriceTarget; this.uncleListLimit = uncleListLimit; this.uncleGenerationLimit = uncleGenerationLimit; this.gasLimit = gasLimit; @@ -62,10 +60,6 @@ public double getGasUnitInDollars() { return minerGasUnitInDollars; } - public long getMinGasPriceTarget() { - return minGasPriceTarget; - } - public int getUncleListLimit() { return uncleListLimit; } diff --git a/rskj-core/src/main/java/co/rsk/config/mining/EthCallMinGasPriceSystemConfig.java b/rskj-core/src/main/java/co/rsk/config/mining/EthCallMinGasPriceSystemConfig.java new file mode 100644 index 00000000000..7c178321dc8 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/config/mining/EthCallMinGasPriceSystemConfig.java @@ -0,0 +1,48 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.config.mining; + +import com.typesafe.config.Config; + +public class EthCallMinGasPriceSystemConfig { + private static final String FROM_PROPERTY = "from"; + private static final String TO_PROPERTY = "to"; + private static final String DATA_PROPERTY = "data"; + private final String address; + private final String from; + private final String data; + + public EthCallMinGasPriceSystemConfig(Config config) { + address = config.getString(TO_PROPERTY); + from = config.getString(FROM_PROPERTY); + data = config.getString(DATA_PROPERTY); + } + + public String getAddress() { + return address; + } + + public String getFrom() { + return from; + } + + public String getData() { + return data; + } +} diff --git a/rskj-core/src/main/java/co/rsk/config/mining/HttpGetStableMinGasSystemConfig.java b/rskj-core/src/main/java/co/rsk/config/mining/HttpGetStableMinGasSystemConfig.java new file mode 100644 index 00000000000..07bdc20d90e --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/config/mining/HttpGetStableMinGasSystemConfig.java @@ -0,0 +1,52 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.config.mining; + +import com.typesafe.config.Config; + +import java.time.Duration; +import java.util.Objects; + +public class HttpGetStableMinGasSystemConfig { + private static final String URL_PROPERTY = "url"; + private static final String JSON_PATH = "jsonPath"; + private static final String TIMEOUT_PROPERTY = "timeout"; + + private final String url; + private final String jsonPath; + private final Duration timeout; + + public HttpGetStableMinGasSystemConfig(Config config) { + this.url = Objects.requireNonNull(config.getString(URL_PROPERTY)); + this.jsonPath = Objects.requireNonNull(config.getString(JSON_PATH)); + this.timeout = Objects.requireNonNull(config.getDuration(TIMEOUT_PROPERTY)); + } + + public String getUrl() { + return url; + } + + public String getJsonPath() { + return jsonPath; + } + + public Duration getTimeout() { + return timeout; + } + +} diff --git a/rskj-core/src/main/java/co/rsk/config/mining/StableMinGasPriceSystemConfig.java b/rskj-core/src/main/java/co/rsk/config/mining/StableMinGasPriceSystemConfig.java new file mode 100644 index 00000000000..36a3fc06318 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/config/mining/StableMinGasPriceSystemConfig.java @@ -0,0 +1,71 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.config.mining; + +import co.rsk.mine.gas.provider.MinGasPriceProviderType; +import com.typesafe.config.Config; + +import java.time.Duration; + +public class StableMinGasPriceSystemConfig { + public static final String STABLE_GAS_PRICE_CONFIG_PATH_PROPERTY = "miner.stableGasPrice"; + + private static final String ENABLED_PROPERTY = "enabled"; + private static final String REFRESH_RATE_PROPERTY = "refreshRate"; + private static final String MIN_STABLE_GAS_PRICE_PROPERTY = "minStableGasPrice"; + private static final String METHOD_PROPERTY = "source.method"; + private static final String PARAMS_PROPERTY = "source.params"; + + private final Duration refreshRate; + private final Long minStableGasPrice; + private final boolean enabled; + private final MinGasPriceProviderType method; + private final Config config; + + public StableMinGasPriceSystemConfig(Config config) { + this.enabled = config.hasPath(ENABLED_PROPERTY) && config.getBoolean(ENABLED_PROPERTY); + this.refreshRate = this.enabled && config.hasPath(REFRESH_RATE_PROPERTY) ? config.getDuration(REFRESH_RATE_PROPERTY) : Duration.ZERO; + this.minStableGasPrice = this.enabled && config.hasPath(MIN_STABLE_GAS_PRICE_PROPERTY) ? config.getLong(MIN_STABLE_GAS_PRICE_PROPERTY) : 0; + this.method = this.enabled ? config.getEnum(MinGasPriceProviderType.class, METHOD_PROPERTY) : MinGasPriceProviderType.FIXED; + this.config = config; + } + + public Duration getRefreshRate() { + return refreshRate; + } + + public Long getMinStableGasPrice() { + return minStableGasPrice; + } + + public boolean isEnabled() { + return enabled; + } + + public HttpGetStableMinGasSystemConfig getHttpGetConfig() { + return new HttpGetStableMinGasSystemConfig(config.getConfig(PARAMS_PROPERTY)); + } + + public EthCallMinGasPriceSystemConfig getEthCallConfig() { + return new EthCallMinGasPriceSystemConfig(config.getConfig(PARAMS_PROPERTY)); + } + + public MinGasPriceProviderType getMethod() { + return method; + } +} diff --git a/rskj-core/src/main/java/co/rsk/mine/MinimumGasPriceCalculator.java b/rskj-core/src/main/java/co/rsk/mine/MinimumGasPriceCalculator.java index d550e14db21..38c16a6c3e8 100644 --- a/rskj-core/src/main/java/co/rsk/mine/MinimumGasPriceCalculator.java +++ b/rskj-core/src/main/java/co/rsk/mine/MinimumGasPriceCalculator.java @@ -19,6 +19,7 @@ package co.rsk.mine; import co.rsk.core.Coin; +import co.rsk.mine.gas.provider.MinGasPriceProvider; /** * This is the implementation of RSKIP-09 @@ -26,14 +27,15 @@ */ public class MinimumGasPriceCalculator { - private final Coin targetMGP; + private MinGasPriceProvider minGasPriceProvider; - public MinimumGasPriceCalculator(Coin targetMGP) { - this.targetMGP = targetMGP; + public MinimumGasPriceCalculator(MinGasPriceProvider minGasPriceProvider) { + this.minGasPriceProvider = minGasPriceProvider; } public Coin calculate(Coin previousMGP) { BlockGasPriceRange priceRange = new BlockGasPriceRange(previousMGP); + Coin targetMGP = minGasPriceProvider.getMinGasPriceAsCoin(); if (priceRange.inRange(targetMGP)) { return targetMGP; } @@ -44,4 +46,5 @@ public Coin calculate(Coin previousMGP) { return priceRange.getLowerLimit(); } + } diff --git a/rskj-core/src/main/java/co/rsk/mine/gas/provider/EthCallMinGasPriceProvider.java b/rskj-core/src/main/java/co/rsk/mine/gas/provider/EthCallMinGasPriceProvider.java new file mode 100644 index 00000000000..e1dce2d3afa --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/mine/gas/provider/EthCallMinGasPriceProvider.java @@ -0,0 +1,95 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.mine.gas.provider; + +import co.rsk.config.mining.EthCallMinGasPriceSystemConfig; +import co.rsk.config.mining.StableMinGasPriceSystemConfig; +import co.rsk.rpc.modules.eth.EthModule; +import co.rsk.util.HexUtils; +import org.ethereum.rpc.parameters.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +public class EthCallMinGasPriceProvider extends StableMinGasPriceProvider { + private static final Logger logger = LoggerFactory.getLogger(EthCallMinGasPriceProvider.class); + + private final String toAddress; + private final String fromAddress; + private final String data; + + private final Supplier ethModuleSupplier; + + public EthCallMinGasPriceProvider(MinGasPriceProvider fallBackProvider, StableMinGasPriceSystemConfig config, Supplier ethModuleSupplier) { + super(fallBackProvider, config.getMinStableGasPrice(), config.getRefreshRate()); + this.ethModuleSupplier = ethModuleSupplier; + EthCallMinGasPriceSystemConfig ethCallConfig = config.getEthCallConfig(); + this.toAddress = ethCallConfig.getAddress(); + this.fromAddress = ethCallConfig.getFrom(); + this.data = ethCallConfig.getData(); + } + + @Override + public MinGasPriceProviderType getType() { + return MinGasPriceProviderType.ETH_CALL; + } + + @Override + protected Optional getBtcExchangeRate() { + EthModule ethModule = Objects.requireNonNull(ethModuleSupplier.get()); + + CallArgumentsParam callArguments = new CallArgumentsParam( + new HexAddressParam(fromAddress), + new HexAddressParam(toAddress), + null, + null, + null, + null, + new HexNumberParam(ethModule.chainId()), + null, + new HexDataParam(data), + null + ); + try { + String callOutput = ethModule.call(callArguments, new BlockIdentifierParam("latest")); + + // Parse the output of the call to get the exchange rate. Will not work with bytes32 values! + return Optional.of(HexUtils.jsonHexToLong( + callOutput)); + } catch (Exception e) { + logger.error("Error calling eth module", e); + return Optional.empty(); + } + } + + public String getToAddress() { + return toAddress; + } + + public String getFromAddress() { + return fromAddress; + } + + public String getData() { + return data; + } +} diff --git a/rskj-core/src/main/java/co/rsk/mine/gas/provider/FixedMinGasPriceProvider.java b/rskj-core/src/main/java/co/rsk/mine/gas/provider/FixedMinGasPriceProvider.java new file mode 100644 index 00000000000..2c20ba731c7 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/mine/gas/provider/FixedMinGasPriceProvider.java @@ -0,0 +1,45 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.mine.gas.provider; + +import co.rsk.core.Coin; + +public class FixedMinGasPriceProvider implements MinGasPriceProvider { + + private final long minGasPrice; + + public FixedMinGasPriceProvider(long minGasPrice) { + this.minGasPrice = minGasPrice; + } + + @Override + public long getMinGasPrice() { + return minGasPrice; + } + + @Override + public MinGasPriceProviderType getType() { + return MinGasPriceProviderType.FIXED; + } + + @Override + public Coin getMinGasPriceAsCoin() { + return Coin.valueOf(minGasPrice); + } +} diff --git a/rskj-core/src/main/java/co/rsk/mine/gas/provider/HttpGetMinGasPriceProvider.java b/rskj-core/src/main/java/co/rsk/mine/gas/provider/HttpGetMinGasPriceProvider.java new file mode 100644 index 00000000000..31c1999e743 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/mine/gas/provider/HttpGetMinGasPriceProvider.java @@ -0,0 +1,94 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.mine.gas.provider; + +import co.rsk.config.mining.StableMinGasPriceSystemConfig; +import co.rsk.config.mining.HttpGetStableMinGasSystemConfig; +import co.rsk.net.http.SimpleHttpClient; +import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +public class HttpGetMinGasPriceProvider extends StableMinGasPriceProvider { + private static final Logger logger = LoggerFactory.getLogger("GasPriceProvider"); + private final String url; + private final JsonPointer jsonPath; + private final SimpleHttpClient httpClient; + private final ObjectMapper objectMapper; + + public HttpGetMinGasPriceProvider(StableMinGasPriceSystemConfig config, MinGasPriceProvider fallBackProvider) { + this(config, fallBackProvider, new SimpleHttpClient(config.getHttpGetConfig().getTimeout().toMillis())); + } + + public HttpGetMinGasPriceProvider(StableMinGasPriceSystemConfig config, MinGasPriceProvider fallBackProvider, SimpleHttpClient httpClient) { + super(fallBackProvider, config.getMinStableGasPrice(), config.getRefreshRate()); + HttpGetStableMinGasSystemConfig httpGetConfig = config.getHttpGetConfig(); + this.url = httpGetConfig.getUrl(); + this.jsonPath = JsonPointer.valueOf(httpGetConfig.getJsonPath()); + this.httpClient = httpClient; + this.objectMapper = new ObjectMapper(); + } + + @Override + protected Optional getBtcExchangeRate() { + String response = getResponseFromWeb(); + if (!StringUtils.isBlank(response)) { + Long price = parsePrice(response); + return Optional.ofNullable(price); + } + return Optional.empty(); + } + + private Long parsePrice(String response) { + try { + JsonNode jObject = objectMapper.readTree(response); + if (jObject.at(jsonPath).isMissingNode()) { + return null; + } + return objectMapper.readTree(response).at(jsonPath).asLong(); + } catch (Exception e) { + logger.error("Error parsing min gas price from web provider", e); + return null; + } + } + + private String getResponseFromWeb() { + try { + return httpClient.doGet(url); + } catch (Exception e) { + logger.error("Error getting min gas price from web provider: {}", e.getMessage()); + } + return null; + } + + @Override + public MinGasPriceProviderType getType() { + return MinGasPriceProviderType.HTTP_GET; + } + + public String getUrl() { + return url; + } + +} + diff --git a/rskj-core/src/main/java/co/rsk/mine/gas/provider/MinGasPriceProvider.java b/rskj-core/src/main/java/co/rsk/mine/gas/provider/MinGasPriceProvider.java new file mode 100644 index 00000000000..688495faae1 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/mine/gas/provider/MinGasPriceProvider.java @@ -0,0 +1,30 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.mine.gas.provider; + +import co.rsk.core.Coin; + +public interface MinGasPriceProvider { + + long getMinGasPrice(); + + MinGasPriceProviderType getType(); + + Coin getMinGasPriceAsCoin(); +} diff --git a/rskj-core/src/main/java/co/rsk/mine/gas/provider/MinGasPriceProviderFactory.java b/rskj-core/src/main/java/co/rsk/mine/gas/provider/MinGasPriceProviderFactory.java new file mode 100644 index 00000000000..844619cf4a5 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/mine/gas/provider/MinGasPriceProviderFactory.java @@ -0,0 +1,65 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.mine.gas.provider; + +import co.rsk.config.mining.StableMinGasPriceSystemConfig; +import co.rsk.rpc.modules.eth.EthModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.function.Supplier; + + +public class MinGasPriceProviderFactory { + + private static final Logger logger = LoggerFactory.getLogger("StableMinGasPrice"); + + private MinGasPriceProviderFactory() { + } + + public static MinGasPriceProvider create(long fixedMinGasPrice, StableMinGasPriceSystemConfig stableMinGasPriceSystemConfig, Supplier ethModuleSupplier) { + FixedMinGasPriceProvider fixedMinGasPriceProvider = new FixedMinGasPriceProvider(fixedMinGasPrice); + + if (stableMinGasPriceSystemConfig == null) { + logger.warn("Could not find stable min gas price system config, using {} provider", fixedMinGasPriceProvider.getType().name()); + return fixedMinGasPriceProvider; + } + if (!stableMinGasPriceSystemConfig.isEnabled()) { + return fixedMinGasPriceProvider; + } + + MinGasPriceProviderType method = stableMinGasPriceSystemConfig.getMethod(); + if (method == null) { + logger.error("Could not find valid method in config, using fallback provider: {}", fixedMinGasPriceProvider.getType().name()); + return fixedMinGasPriceProvider; + } + + switch (method) { + case HTTP_GET: + return new HttpGetMinGasPriceProvider(stableMinGasPriceSystemConfig, fixedMinGasPriceProvider); + case ETH_CALL: + return new EthCallMinGasPriceProvider(fixedMinGasPriceProvider, stableMinGasPriceSystemConfig, ethModuleSupplier); + case FIXED: + return fixedMinGasPriceProvider; + default: + logger.debug("Could not find a valid implementation for the method {}. Returning fallback provider {}", method, fixedMinGasPriceProvider.getType().name()); + return fixedMinGasPriceProvider; + } + } +} diff --git a/rskj-core/src/main/java/co/rsk/mine/gas/provider/MinGasPriceProviderType.java b/rskj-core/src/main/java/co/rsk/mine/gas/provider/MinGasPriceProviderType.java new file mode 100644 index 00000000000..0d6f2e7cf99 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/mine/gas/provider/MinGasPriceProviderType.java @@ -0,0 +1,25 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.mine.gas.provider; + +public enum MinGasPriceProviderType { + FIXED, + HTTP_GET, + ETH_CALL +} diff --git a/rskj-core/src/main/java/co/rsk/mine/gas/provider/StableMinGasPriceProvider.java b/rskj-core/src/main/java/co/rsk/mine/gas/provider/StableMinGasPriceProvider.java new file mode 100644 index 00000000000..72f83d02a74 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/mine/gas/provider/StableMinGasPriceProvider.java @@ -0,0 +1,141 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.mine.gas.provider; + +import co.rsk.core.Coin; +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +public abstract class StableMinGasPriceProvider implements MinGasPriceProvider { + private static final Logger logger = LoggerFactory.getLogger("StableMinGasPrice"); + private static final int ERR_NUM_OF_FAILURES = 20; + + private final MinGasPriceProvider fallBackProvider; + private final long minStableGasPrice; + private final long refreshRateInMillis; + private final AtomicInteger numOfFailures = new AtomicInteger(); + private final AtomicReference> priceFuture = new AtomicReference<>(); + + private volatile long lastMinGasPrice; + private volatile long lastUpdateTimeMillis; + + protected StableMinGasPriceProvider(MinGasPriceProvider fallBackProvider, long minStableGasPrice, Duration refreshRate) { + this.minStableGasPrice = minStableGasPrice; + this.fallBackProvider = fallBackProvider; + this.refreshRateInMillis = refreshRate.toMillis(); + } + + protected abstract Optional getBtcExchangeRate(); + + @Override + public long getMinGasPrice() { + return getMinGasPrice(false); + } + + @VisibleForTesting + public long getMinGasPrice(boolean wait) { + long currentTimeMillis = System.currentTimeMillis(); + if (currentTimeMillis - lastUpdateTimeMillis >= refreshRateInMillis) { + Future priceFuture = fetchPriceAsync(); + if (wait || priceFuture.isDone()) { + try { + return priceFuture.get(); + } catch (InterruptedException e) { + logger.error("Min gas price fetching was interrupted", e); + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + logger.error("Min gas price fetching was failed", e); + } + } + } + + return getLastMinGasPrice(); + } + + @Override + public Coin getMinGasPriceAsCoin() { + return Coin.valueOf(getMinGasPrice()); + } + + @VisibleForTesting + Coin getMinGasPriceAsCoin(boolean wait) { + return Coin.valueOf(getMinGasPrice(wait)); + } + + private long getLastMinGasPrice() { + if (lastMinGasPrice > 0) { + return lastMinGasPrice; + } + + return fallBackProvider.getMinGasPrice(); + } + + private long calculateMinGasPriceBasedOnBtcPrice(long btcValue) { + if (minStableGasPrice == 0 || btcValue == 0) { + return 0; + } + return minStableGasPrice / btcValue; + } + + private synchronized Future fetchPriceAsync() { + Future future = priceFuture.get(); + if (future != null) { + return future; + } + + CompletableFuture newFuture = new CompletableFuture<>(); + priceFuture.set(newFuture); + + new Thread(() -> { + Optional priceResponse = fetchPriceSync(); + newFuture.complete(priceResponse.orElse(getLastMinGasPrice())); + priceFuture.set(null); + }).start(); + + return newFuture; + } + + private Optional fetchPriceSync() { + Optional priceResponse = getBtcExchangeRate(); + if (priceResponse.isPresent() && priceResponse.get() > 0) { + long result = calculateMinGasPriceBasedOnBtcPrice(priceResponse.get()); + lastMinGasPrice = result; + lastUpdateTimeMillis = System.currentTimeMillis(); + numOfFailures.set(0); + return Optional.of(result); + } + + int failedAttempts = numOfFailures.incrementAndGet(); + if (failedAttempts >= ERR_NUM_OF_FAILURES) { + logger.error("Gas price was not updated as it was not possible to obtain valid price from provider. Check your provider setup. Number of failed attempts: {}", failedAttempts); + } else { + logger.warn("Gas price was not updated as it was not possible to obtain valid price from provider. Number of failed attempts: {}", failedAttempts); + } + + return Optional.empty(); + } +} diff --git a/rskj-core/src/main/java/co/rsk/net/http/HttpException.java b/rskj-core/src/main/java/co/rsk/net/http/HttpException.java new file mode 100644 index 00000000000..c7c77b54414 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/net/http/HttpException.java @@ -0,0 +1,28 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.net.http; + +public class HttpException extends Exception { + + private static final long serialVersionUID = 9209168000899420627L; + + public HttpException(String message) { + super(message); + } + +} diff --git a/rskj-core/src/main/java/co/rsk/net/http/SimpleHttpClient.java b/rskj-core/src/main/java/co/rsk/net/http/SimpleHttpClient.java new file mode 100644 index 00000000000..b287247349c --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/net/http/SimpleHttpClient.java @@ -0,0 +1,75 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.net.http; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.UnknownHostException; + +public class SimpleHttpClient { + private static final Logger logger = LoggerFactory.getLogger("simpleHttp"); + private static final String GET_METHOD = "GET"; + private final int timeoutMillis; + + public SimpleHttpClient(long timeoutMillis) { + if (timeoutMillis > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Timeout value is too large."); + } + this.timeoutMillis = (int) timeoutMillis; + } + + public String doGet(String targetUrl) throws HttpException { + StringBuilder response = new StringBuilder(); + try { + HttpURLConnection conn = (HttpURLConnection) new URL(targetUrl).openConnection(); + conn.setRequestMethod(GET_METHOD); + conn.setConnectTimeout(timeoutMillis); + conn.setReadTimeout(timeoutMillis); + int responseCode = conn.getResponseCode(); + + if (responseCode >= 300 || responseCode < 200) { + String responseMessage = conn.getResponseMessage(); + throw new HttpException("Http request failed with code : " + responseCode + " - " + responseMessage); + } + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + } + } catch (HttpException httpException) { + throw httpException; + } catch (UnknownHostException unknownHostException) { + logger.error("Unknown host from url: {}. {}", targetUrl, unknownHostException.getMessage()); + throw new HttpException("Unknown host from url: " + targetUrl); + } catch (Exception e) { + logger.error("Http request failed.", e); + throw new HttpException("Http request failed with error : " + e.getMessage()); + } + + return response.toString(); + } + + +} diff --git a/rskj-core/src/main/java/org/ethereum/config/SystemProperties.java b/rskj-core/src/main/java/org/ethereum/config/SystemProperties.java index 0f30adb8b27..26e97fcfc94 100644 --- a/rskj-core/src/main/java/org/ethereum/config/SystemProperties.java +++ b/rskj-core/src/main/java/org/ethereum/config/SystemProperties.java @@ -21,6 +21,7 @@ import co.rsk.bitcoinj.core.BtcECKey; import co.rsk.config.ConfigLoader; +import co.rsk.config.mining.StableMinGasPriceSystemConfig; import com.typesafe.config.Config; import com.typesafe.config.ConfigObject; import com.typesafe.config.ConfigRenderOptions; @@ -126,10 +127,12 @@ public abstract class SystemProperties { protected SystemProperties(ConfigLoader loader) { try { this.configFromFiles = loader.getConfig(); - logger.trace( - "Config trace: {}", - configFromFiles.root().render(ConfigRenderOptions.defaults().setComments(false).setJson(false)) - ); + if(logger.isTraceEnabled()){ + logger.trace( + "Config trace: {}", + configFromFiles.root().render(ConfigRenderOptions.defaults().setComments(false).setJson(false)) + ); + } Properties props = new Properties(); try (InputStream is = getClass().getResourceAsStream("/version.properties")) { @@ -150,11 +153,11 @@ private static String getProjectVersion(Properties props) { return "-.-.-"; } - return versionNumber.replaceAll("'", ""); + return versionNumber.replace("'", ""); } private static String getProjectVersionModifier(Properties props) { - return props.getProperty("modifier").replaceAll("\"", ""); + return props.getProperty("modifier").replace("\"", ""); } public Config getConfig() { @@ -402,8 +405,10 @@ private String getGeneratedNodePrivateKey() { file.getParentFile().mkdirs(); try (FileWriter writer = new FileWriter(file)) { props.store(writer, "Generated NodeID. To use your own nodeId please refer to 'peer.privateKey' config option."); - logger.info("New nodeID generated: {}", props.getProperty("nodeId")); - logger.info("Generated nodeID and its private key stored in {}", file); + if(logger.isInfoEnabled()) { + logger.info("New nodeID generated: {}", props.getProperty("nodeId")); + logger.info("Generated nodeID and its private key stored in {}", file); + } } } return props.getProperty("nodeIdPrivateKey"); @@ -501,10 +506,8 @@ private InetAddress getMyPublicIpFromRemoteService() { InetAddress resolvedIp = tryParseIpOrThrow(ipFromService); logger.info("Identified public IP: {}", resolvedIp); return resolvedIp; - } catch (IOException e) { - logger.error("Can't get public IP", e); - } catch (IllegalArgumentException e) { - logger.error("Can't get public IP", e); + } catch (IOException | IllegalArgumentException exception) { + logger.error("Can't get public IP", exception); } InetAddress bindAddress = getBindAddress(); @@ -765,4 +768,9 @@ public int getRpcMaxResponseSize() { return configFromFiles.getInt(PROPERTY_RPC_MAX_RESPONSE_SIZE); } + + public StableMinGasPriceSystemConfig getStableGasPriceSystemConfig() { + Config config = configFromFiles.getConfig(StableMinGasPriceSystemConfig.STABLE_GAS_PRICE_CONFIG_PATH_PROPERTY); + return new StableMinGasPriceSystemConfig(config); + } } diff --git a/rskj-core/src/main/resources/expected.conf b/rskj-core/src/main/resources/expected.conf index 5b440a1367e..41dba8f87c3 100644 --- a/rskj-core/src/main/resources/expected.conf +++ b/rskj-core/src/main/resources/expected.conf @@ -189,6 +189,22 @@ miner = { delayBetweenBlocks = delayBetweenRefreshes = } + stableGasPrice { + enabled = + source { + method = + params { + url = + jsonPath = + timeout = + from = + to = + data = + } + } + minStableGasPrice = + refreshRate = + } coinbase.secret = reward.address =
gasUnitInDollars = diff --git a/rskj-core/src/main/resources/reference.conf b/rskj-core/src/main/resources/reference.conf index 8228aa620d1..3293fd6e326 100644 --- a/rskj-core/src/main/resources/reference.conf +++ b/rskj-core/src/main/resources/reference.conf @@ -201,6 +201,37 @@ miner { # The time the miner client will wait to refresh the current work from the miner server delayBetweenRefreshes = 1 second } + + stableGasPrice { + enabled = false + + # source { + # # method options: HTTP_GET | ETH_CALL + # # Examples: + # + # method = "HTTP_GET" + # params { + # url = "https://domain.info/ticker" + # jsonPath = "/USD/buy" + # timeout = 2 seconds # Value's type is `Duration`, e.g. 1.1 seconds, 2.5 minutes, 3 hours, 4 days. + # } + # + # # OR + # + # method = "ETH_CALL" + # params { + # from: "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd825", + # to: "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", + # data: "0x8300df49" + # } + # } + # + # minStableGasPrice = 4265280000000 # 0.00000426528 USD per gas unit + # + # # The time the miner client will wait to refresh price from the source. + # # Value's type is `Duration`, e.g. 1.1 seconds, 2.5 minutes, 3 hours, 4 days. + # refreshRate = 6 hours + } } database { diff --git a/rskj-core/src/test/java/co/rsk/blockchain/utils/BlockGenerator.java b/rskj-core/src/test/java/co/rsk/blockchain/utils/BlockGenerator.java index 03360b9cfeb..038ee120217 100644 --- a/rskj-core/src/test/java/co/rsk/blockchain/utils/BlockGenerator.java +++ b/rskj-core/src/test/java/co/rsk/blockchain/utils/BlockGenerator.java @@ -26,6 +26,7 @@ import co.rsk.core.bc.BlockHashesHelper; import co.rsk.crypto.Keccak256; import co.rsk.mine.MinimumGasPriceCalculator; +import co.rsk.mine.gas.provider.FixedMinGasPriceProvider; import co.rsk.peg.PegTestUtils; import co.rsk.peg.simples.SimpleRskTransaction; import org.bouncycastle.pqc.math.linearalgebra.ByteUtils; @@ -313,7 +314,7 @@ public Block createBlock(long number, int ntxs, Long gasLimit){ } Coin previousMGP = parent.getMinimumGasPrice() != null ? parent.getMinimumGasPrice() : Coin.valueOf(10L); - Coin minimumGasPrice = new MinimumGasPriceCalculator(Coin.valueOf(100L)).calculate(previousMGP); + Coin minimumGasPrice = new MinimumGasPriceCalculator(new FixedMinGasPriceProvider(100L)).calculate(previousMGP); boolean isRskip126Enabled = activationConfig.isActive(ConsensusRule.RSKIP126, 0); diff --git a/rskj-core/src/test/java/co/rsk/config/ConfigUtils.java b/rskj-core/src/test/java/co/rsk/config/ConfigUtils.java index 5020b165d82..8a7ba3829ba 100644 --- a/rskj-core/src/test/java/co/rsk/config/ConfigUtils.java +++ b/rskj-core/src/test/java/co/rsk/config/ConfigUtils.java @@ -26,7 +26,6 @@ public static MiningConfig getDefaultMiningConfig() { new RskAddress(coinbaseAddress), 0.0, 1.0, - 0, 10, 7, new GasLimitConfig(3000000, 500000, true), diff --git a/rskj-core/src/test/java/co/rsk/config/mining/EthCallMinGasPriceSystemConfigTest.java b/rskj-core/src/test/java/co/rsk/config/mining/EthCallMinGasPriceSystemConfigTest.java new file mode 100644 index 00000000000..295abaec0ce --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/config/mining/EthCallMinGasPriceSystemConfigTest.java @@ -0,0 +1,58 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.config.mining; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class EthCallMinGasPriceSystemConfigTest { + private EthCallMinGasPriceSystemConfig config; + private String to = "0x77045E71a7A2c50903d88e564cD72fab11e82051"; + private String from = "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"; + private String data = "0x98d5fdca"; + + @BeforeEach + void setUp() { + Config testConfig = ConfigFactory.parseString( + "to=\"" + to + "\"\n" + + "from=\"" + from + "\"\n" + + "data=\"" + data + "\"" + ); + config = new EthCallMinGasPriceSystemConfig(testConfig); + } + + @Test + void testAddress() { + assertEquals(to, config.getAddress()); + } + + @Test + void testFrom() { + assertEquals(from, config.getFrom()); + } + + @Test + void testData() { + assertEquals(data, config.getData()); + } +} diff --git a/rskj-core/src/test/java/co/rsk/config/mining/HttpGetStableMinGasSystemConfigTest.java b/rskj-core/src/test/java/co/rsk/config/mining/HttpGetStableMinGasSystemConfigTest.java new file mode 100644 index 00000000000..ffde165c373 --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/config/mining/HttpGetStableMinGasSystemConfigTest.java @@ -0,0 +1,56 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.config.mining; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; + +class HttpGetStableMinGasSystemConfigTest { + private HttpGetStableMinGasSystemConfig config; + + @BeforeEach + void setUp() { + Config testConfig = ConfigFactory.parseString( + "url=\"http://test.url\"\n" + + "jsonPath=testPath\n" + + "timeout=1000 milliseconds\n" + ); + config = new HttpGetStableMinGasSystemConfig(testConfig); + } + + @Test + void testGetUrl() { + assertEquals("http://test.url", config.getUrl()); + } + + @Test + void testRequestPath() { + assertEquals("testPath", config.getJsonPath()); + } + + @Test + void testGetTimeout() { + assertEquals(Duration.ofMillis(1000), config.getTimeout()); + } +} \ No newline at end of file diff --git a/rskj-core/src/test/java/co/rsk/config/mining/StableMinGasPriceSystemConfigTest.java b/rskj-core/src/test/java/co/rsk/config/mining/StableMinGasPriceSystemConfigTest.java new file mode 100644 index 00000000000..77da16e799d --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/config/mining/StableMinGasPriceSystemConfigTest.java @@ -0,0 +1,96 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.config.mining; + +import co.rsk.mine.gas.provider.MinGasPriceProviderType; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigException; +import com.typesafe.config.ConfigFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; + +class StableMinGasPriceSystemConfigTest { + + private StableMinGasPriceSystemConfig config; + + @BeforeEach + void setUp() { + Config testConfig = ConfigFactory.parseString( + "enabled=true\n" + + "refreshRate=10 hours\n" + + "minStableGasPrice=100\n" + + "source={ method=FIXED }" + ); + config = new StableMinGasPriceSystemConfig(testConfig); + } + + @Test + void testGetRefreshRate() { + assertEquals(Duration.ofHours(10), config.getRefreshRate()); + } + + @Test + void testGetMinStableGasPrice() { + assertEquals(100, config.getMinStableGasPrice()); + } + + @Test + void testIsEnabled() { + assertTrue(config.isEnabled()); + } + + @Test + void testGetMethod() { + assertEquals(MinGasPriceProviderType.FIXED, config.getMethod()); + } + + @Test + void wrongMethodShouldThrowException() { + Config testConfig = ConfigFactory.parseString( + "enabled=true\n" + + "refreshRate=10\n" + + "minStableGasPrice=100\n" + + "source={ method=INVALID }" + ); + assertThrows( + ConfigException.BadValue.class, + () -> new StableMinGasPriceSystemConfig(testConfig), + "Expected to throw Config exception, but it didn't" + ); + } + + @Test + void missingPropertyShouldThrowException() { + Config testConfig = ConfigFactory.parseString( + "enabled=true\n" + + "minStableGasPrice=100\n" + + "method=FIXED" + ); + assertThrows( + ConfigException.Missing.class, + () -> new StableMinGasPriceSystemConfig(testConfig), + "Expected to throw Config exception, but it didn't" + ); + } + + +} \ No newline at end of file diff --git a/rskj-core/src/test/java/co/rsk/mine/MainNetMinerTest.java b/rskj-core/src/test/java/co/rsk/mine/MainNetMinerTest.java index 63a427680c5..8abb85f99a4 100644 --- a/rskj-core/src/test/java/co/rsk/mine/MainNetMinerTest.java +++ b/rskj-core/src/test/java/co/rsk/mine/MainNetMinerTest.java @@ -22,7 +22,6 @@ import co.rsk.config.MiningConfig; import co.rsk.config.TestSystemProperties; import co.rsk.core.BlockDifficulty; -import co.rsk.core.Coin; import co.rsk.core.DifficultyCalculator; import co.rsk.core.bc.BlockChainImpl; import co.rsk.core.bc.BlockChainImplTest; @@ -30,6 +29,7 @@ import co.rsk.core.bc.MiningMainchainView; import co.rsk.core.genesis.TestGenesisLoader; import co.rsk.db.RepositoryLocator; +import co.rsk.mine.gas.provider.FixedMinGasPriceProvider; import co.rsk.net.NodeBlockProcessor; import co.rsk.test.builders.BlockChainBuilder; import co.rsk.validators.BlockUnclesValidationRule; @@ -239,7 +239,7 @@ private BlockToMineBuilder blockToMineBuilder() { clock, blockFactory, blockExecutor, - new MinimumGasPriceCalculator(Coin.valueOf(miningConfig.getMinGasPriceTarget())), + new MinimumGasPriceCalculator(new FixedMinGasPriceProvider(config.minerMinGasPrice())), new MinerUtils(), new BlockTxSignatureCache(new ReceivedTxSignatureCache()) ); diff --git a/rskj-core/src/test/java/co/rsk/mine/MinerManagerTest.java b/rskj-core/src/test/java/co/rsk/mine/MinerManagerTest.java index 787e6a39fa7..9d436ad49e3 100644 --- a/rskj-core/src/test/java/co/rsk/mine/MinerManagerTest.java +++ b/rskj-core/src/test/java/co/rsk/mine/MinerManagerTest.java @@ -20,12 +20,12 @@ import co.rsk.config.ConfigUtils; import co.rsk.config.MiningConfig; import co.rsk.config.TestSystemProperties; -import co.rsk.core.Coin; import co.rsk.core.DifficultyCalculator; import co.rsk.core.SnapshotManager; import co.rsk.core.bc.BlockExecutor; import co.rsk.core.bc.MiningMainchainView; import co.rsk.db.RepositoryLocator; +import co.rsk.mine.gas.provider.FixedMinGasPriceProvider; import co.rsk.net.NodeBlockProcessor; import co.rsk.validators.BlockValidationRule; import co.rsk.validators.ProofOfWorkRule; @@ -291,7 +291,7 @@ private MinerServerImpl getMinerServer() { clock, blockFactory, blockExecutor, - new MinimumGasPriceCalculator(Coin.valueOf(miningConfig.getMinGasPriceTarget())), + new MinimumGasPriceCalculator(new FixedMinGasPriceProvider(config.minerMinGasPrice())), new MinerUtils(), new BlockTxSignatureCache(new ReceivedTxSignatureCache()) // TODO -> should it be ReceivedTxSignatureCache? See Miner Server Test for reference ), diff --git a/rskj-core/src/test/java/co/rsk/mine/MinerServerTest.java b/rskj-core/src/test/java/co/rsk/mine/MinerServerTest.java index cdca5ceea1b..8eac7ab4bec 100644 --- a/rskj-core/src/test/java/co/rsk/mine/MinerServerTest.java +++ b/rskj-core/src/test/java/co/rsk/mine/MinerServerTest.java @@ -29,6 +29,7 @@ import co.rsk.core.bc.*; import co.rsk.crypto.Keccak256; import co.rsk.db.RepositoryLocator; +import co.rsk.mine.gas.provider.FixedMinGasPriceProvider; import co.rsk.net.BlockProcessor; import co.rsk.net.NodeBlockProcessor; import co.rsk.net.handler.quota.TxQuotaChecker; @@ -130,7 +131,7 @@ protected RepositoryLocator buildRepositoryLocator() { blockFactory = rskTestContext.getBlockFactory(); blockExecutor = rskTestContext.getBlockExecutor(); - minimumGasPriceCalculator = new MinimumGasPriceCalculator(Coin.ZERO); + minimumGasPriceCalculator = new MinimumGasPriceCalculator(new FixedMinGasPriceProvider(0L)); minerUtils = new MinerUtils(); } diff --git a/rskj-core/src/test/java/co/rsk/mine/MinimumGasPriceCalculatorTest.java b/rskj-core/src/test/java/co/rsk/mine/MinimumGasPriceCalculatorTest.java index 50c83b5e329..054656b6743 100644 --- a/rskj-core/src/test/java/co/rsk/mine/MinimumGasPriceCalculatorTest.java +++ b/rskj-core/src/test/java/co/rsk/mine/MinimumGasPriceCalculatorTest.java @@ -19,19 +19,33 @@ package co.rsk.mine; import co.rsk.core.Coin; +import co.rsk.mine.gas.provider.MinGasPriceProvider; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.mockito.Mockito.when; /** * Created by mario on 22/12/16. */ class MinimumGasPriceCalculatorTest { + private MinGasPriceProvider minGasPriceProvider; + private MinimumGasPriceCalculator mgpCalculator; + + @BeforeEach + void init() { + minGasPriceProvider = Mockito.mock(MinGasPriceProvider.class); + mgpCalculator = new MinimumGasPriceCalculator(minGasPriceProvider); + } + @Test void increaseMgp() { Coin target = Coin.valueOf(2000L); Coin prev = Coin.valueOf(1000L); - MinimumGasPriceCalculator mgpCalculator = new MinimumGasPriceCalculator(target); + when(minGasPriceProvider.getMinGasPriceAsCoin()).thenReturn(target); Coin mgp = mgpCalculator.calculate(prev); Assertions.assertEquals(Coin.valueOf(1010), mgp); } @@ -40,7 +54,7 @@ void increaseMgp() { void decreaseMGP() { Coin prev = Coin.valueOf(1000L); Coin target = Coin.valueOf(900L); - MinimumGasPriceCalculator mgpCalculator = new MinimumGasPriceCalculator(target); + when(minGasPriceProvider.getMinGasPriceAsCoin()).thenReturn(target); Coin mgp = mgpCalculator.calculate(prev); Assertions.assertEquals(Coin.valueOf(990), mgp); } @@ -49,7 +63,7 @@ void decreaseMGP() { void mgpOnRage() { Coin prev = Coin.valueOf(1000L); Coin target = Coin.valueOf(995L); - MinimumGasPriceCalculator mgpCalculator = new MinimumGasPriceCalculator(target); + when(minGasPriceProvider.getMinGasPriceAsCoin()).thenReturn(target); Coin mgp = mgpCalculator.calculate(prev); Assertions.assertEquals(target, mgp); } @@ -58,7 +72,7 @@ void mgpOnRage() { void previousMgpEqualsTarget() { Coin prev = Coin.valueOf(1000L); Coin target = Coin.valueOf(1000L); - MinimumGasPriceCalculator mgpCalculator = new MinimumGasPriceCalculator(target); + when(minGasPriceProvider.getMinGasPriceAsCoin()).thenReturn(target); Coin mgp = mgpCalculator.calculate(prev); Assertions.assertEquals(target, mgp); } @@ -66,7 +80,7 @@ void previousMgpEqualsTarget() { @Test void previousValueIsZero() { Coin target = Coin.valueOf(1000L); - MinimumGasPriceCalculator mgpCalculator = new MinimumGasPriceCalculator(target); + when(minGasPriceProvider.getMinGasPriceAsCoin()).thenReturn(target); Coin mgp = mgpCalculator.calculate(Coin.ZERO); Assertions.assertEquals(Coin.valueOf(1L), mgp); } @@ -74,7 +88,7 @@ void previousValueIsZero() { @Test void previousValueIsSmallTargetIsZero() { Coin target = Coin.ZERO; - MinimumGasPriceCalculator mgpCalculator = new MinimumGasPriceCalculator(target); + when(minGasPriceProvider.getMinGasPriceAsCoin()).thenReturn(target); Coin mgp = mgpCalculator.calculate(Coin.valueOf(1L)); Assertions.assertEquals(Coin.ZERO, mgp); } @@ -83,8 +97,9 @@ void previousValueIsSmallTargetIsZero() { void cantGetMGPtoBeNegative() { Coin previous = Coin.ZERO; Coin target = Coin.valueOf(-100L); - MinimumGasPriceCalculator mgpCalculator = new MinimumGasPriceCalculator(target); + when(minGasPriceProvider.getMinGasPriceAsCoin()).thenReturn(target); previous = mgpCalculator.calculate(previous); Assertions.assertTrue(previous.compareTo(Coin.valueOf(-1)) > 0); } + } diff --git a/rskj-core/src/test/java/co/rsk/mine/TransactionModuleTest.java b/rskj-core/src/test/java/co/rsk/mine/TransactionModuleTest.java index 1d2f6641773..e2ff31e8e8e 100644 --- a/rskj-core/src/test/java/co/rsk/mine/TransactionModuleTest.java +++ b/rskj-core/src/test/java/co/rsk/mine/TransactionModuleTest.java @@ -26,6 +26,7 @@ import co.rsk.db.RepositorySnapshot; import co.rsk.db.StateRootHandler; import co.rsk.db.StateRootsStoreImpl; +import co.rsk.mine.gas.provider.FixedMinGasPriceProvider; import co.rsk.net.TransactionGateway; import co.rsk.net.handler.quota.TxQuotaChecker; import co.rsk.peg.BridgeSupportFactory; @@ -612,7 +613,7 @@ private Web3Impl internalCreateEnvironment(Blockchain blockchain, minerClock, blockFactory, blockExecutor, - new MinimumGasPriceCalculator(Coin.valueOf(miningConfig.getMinGasPriceTarget())), + new MinimumGasPriceCalculator(new FixedMinGasPriceProvider(config.minerMinGasPrice())), new MinerUtils(), new BlockTxSignatureCache(new ReceivedTxSignatureCache()) ), diff --git a/rskj-core/src/test/java/co/rsk/mine/gas/FixedMinGasPriceProviderTest.java b/rskj-core/src/test/java/co/rsk/mine/gas/FixedMinGasPriceProviderTest.java new file mode 100644 index 00000000000..8fef8654e1c --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/mine/gas/FixedMinGasPriceProviderTest.java @@ -0,0 +1,50 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.mine.gas; + +import co.rsk.core.Coin; +import co.rsk.mine.gas.provider.FixedMinGasPriceProvider; +import co.rsk.mine.gas.provider.MinGasPriceProviderType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class FixedMinGasPriceProviderTest { + + @Test + void testConstructorAndGetMinGasPrice() { + long minGasPrice = 100L; + FixedMinGasPriceProvider provider = new FixedMinGasPriceProvider(minGasPrice); + assertEquals(minGasPrice, provider.getMinGasPrice()); + } + + @Test + void testGetType() { + long minGasPrice = 100L; + FixedMinGasPriceProvider provider = new FixedMinGasPriceProvider(minGasPrice); + assertEquals(MinGasPriceProviderType.FIXED, provider.getType()); + } + + @Test + void testGetMinGasPriceAsCoin() { + long minGasPrice = 50; + FixedMinGasPriceProvider provider = new FixedMinGasPriceProvider(minGasPrice); + assertEquals(Coin.valueOf(minGasPrice), provider.getMinGasPriceAsCoin()); + } +} diff --git a/rskj-core/src/test/java/co/rsk/mine/gas/provider/EthCallMinGasPriceProviderTest.java b/rskj-core/src/test/java/co/rsk/mine/gas/provider/EthCallMinGasPriceProviderTest.java new file mode 100644 index 00000000000..25ac24e9c52 --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/mine/gas/provider/EthCallMinGasPriceProviderTest.java @@ -0,0 +1,147 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.mine.gas.provider; + +import co.rsk.config.mining.EthCallMinGasPriceSystemConfig; +import co.rsk.config.mining.StableMinGasPriceSystemConfig; +import co.rsk.rpc.modules.eth.EthModule; +import co.rsk.util.HexUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +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.Mockito.mock; +import static org.mockito.Mockito.when; + +class EthCallMinGasPriceProviderTest { + private EthModule ethModule_mock; + private MinGasPriceProvider fallback_mock; + private EthCallMinGasPriceSystemConfig ethCallMinGasPriceSystemConfig_mock; + private EthCallMinGasPriceProvider ethCallMinGasPriceProvider; + private StableMinGasPriceSystemConfig stableMinGasPriceSystemConfig; + + @BeforeEach + public void beforeEach() { + ethModule_mock = mock(EthModule.class); + when(ethModule_mock.chainId()).thenReturn("0x21"); + + fallback_mock = mock(MinGasPriceProvider.class); + when(fallback_mock.getType()).thenReturn(MinGasPriceProviderType.FIXED); + long fallback_minGasPrice_fake = 1234567890L; + when(fallback_mock.getMinGasPrice()).thenReturn(fallback_minGasPrice_fake); + + ethCallMinGasPriceSystemConfig_mock = mock(EthCallMinGasPriceSystemConfig.class); + String oracle_address = "0xbffBD993FF1d229B0FfE55668F2009d20d4F7C5f"; + when(ethCallMinGasPriceSystemConfig_mock.getAddress()).thenReturn(oracle_address); + String from_address = "0xbffBD993FF1d229B0FfE55668F2009d20d4F7C5f"; + when(ethCallMinGasPriceSystemConfig_mock.getFrom()).thenReturn(from_address); + String data = "0x"; + when(ethCallMinGasPriceSystemConfig_mock.getData()).thenReturn(data); + + stableMinGasPriceSystemConfig = mock(StableMinGasPriceSystemConfig.class); + when(stableMinGasPriceSystemConfig.getEthCallConfig()).thenReturn(ethCallMinGasPriceSystemConfig_mock); + when(stableMinGasPriceSystemConfig.getMinStableGasPrice()).thenReturn(fallback_minGasPrice_fake); + + + ethCallMinGasPriceProvider = new EthCallMinGasPriceProvider( + fallback_mock, + stableMinGasPriceSystemConfig, + () -> ethModule_mock + ); + } + + @AfterEach + public void afterEach() { + ethModule_mock = null; + fallback_mock = null; + } + + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"0x123", "0xabc"}) + void constructorSetsFieldsCorrectly(String data_input) { + MinGasPriceProvider fallbackProvider = mock(MinGasPriceProvider.class); + EthCallMinGasPriceSystemConfig config = stableMinGasPriceSystemConfig.getEthCallConfig(); + + when(config.getAddress()).thenReturn("0xaddress"); + when(config.getFrom()).thenReturn("0xfrom"); + when(config.getData()).thenReturn(data_input); + + EthCallMinGasPriceProvider provider = new EthCallMinGasPriceProvider(fallbackProvider, stableMinGasPriceSystemConfig, () -> ethModule_mock); + + Assertions.assertEquals("0xaddress", provider.getToAddress()); + } + + @Test + void constructorSetsFieldsToNullWhenConfigReturnsNull() { + MinGasPriceProvider fallbackProvider = mock(MinGasPriceProvider.class); + EthCallMinGasPriceSystemConfig config = stableMinGasPriceSystemConfig.getEthCallConfig(); + when(config.getAddress()).thenReturn(null); + when(config.getFrom()).thenReturn(null); + when(config.getData()).thenReturn(null); + + + EthCallMinGasPriceProvider provider = new EthCallMinGasPriceProvider(fallbackProvider, stableMinGasPriceSystemConfig, () -> ethModule_mock); + + Assertions.assertNull(provider.getToAddress()); + Assertions.assertNull(provider.getFromAddress()); + Assertions.assertNull(provider.getData()); + } + + @Test + void getStableMinGasPrice_callsEthModulesCallMethod() { + String expectedPrice = "0x21"; + when(ethModule_mock.call(any(), any())).thenReturn(expectedPrice); + + assertTrue(ethCallMinGasPriceProvider.getBtcExchangeRate().isPresent()); + Assertions.assertEquals( + HexUtils.jsonHexToLong(expectedPrice), + ethCallMinGasPriceProvider.getBtcExchangeRate().get() + ); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", "0x"}) + void getStableMinGasPrice_callsFallback_whenNoData(String data_input) { + when(ethCallMinGasPriceSystemConfig_mock.getData()).thenReturn(data_input); + + assertFalse(ethCallMinGasPriceProvider.getBtcExchangeRate().isPresent()); + } + + + @Test + void getStableMinGasPrice_callsFallback_whenEthModuleIsNull() { + assertFalse(ethCallMinGasPriceProvider.getBtcExchangeRate().isPresent()); + } + + @Test + void getType_returnsOnChain() { + Assertions.assertEquals(MinGasPriceProviderType.ETH_CALL, ethCallMinGasPriceProvider.getType()); + } + +} diff --git a/rskj-core/src/test/java/co/rsk/mine/gas/provider/HttpGetMinGasPriceProviderTest.java b/rskj-core/src/test/java/co/rsk/mine/gas/provider/HttpGetMinGasPriceProviderTest.java new file mode 100644 index 00000000000..231ff946ca1 --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/mine/gas/provider/HttpGetMinGasPriceProviderTest.java @@ -0,0 +1,135 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.mine.gas.provider; + +import co.rsk.config.mining.HttpGetStableMinGasSystemConfig; +import co.rsk.config.mining.StableMinGasPriceSystemConfig; +import co.rsk.net.http.HttpException; +import co.rsk.net.http.SimpleHttpClient; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +class HttpGetMinGasPriceProviderTest { + + @Test + void returnsMappedPriceFromWebClient() throws HttpException { + MinGasPriceProvider fallbackProvider = mock(MinGasPriceProvider.class); + when(fallbackProvider.getType()).thenReturn(MinGasPriceProviderType.FIXED); + + SimpleHttpClient httpClient = mock(SimpleHttpClient.class); + when(httpClient.doGet(anyString())).thenReturn("{\"bitcoin\":{\"usd\":10000}}"); + StableMinGasPriceSystemConfig config = createStableMinGasPriceSystemConfig(); + HttpGetMinGasPriceProvider provider = new HttpGetMinGasPriceProvider(config, fallbackProvider, httpClient); + + Optional result = provider.getBtcExchangeRate(); + verify(fallbackProvider, times(0)).getMinGasPrice(); + assertTrue(result.isPresent()); + assertEquals(10000L, result.get()); + } + + @Test + void whenRequestingTheValueTwiceCachedValueIsUsed() throws HttpException { + MinGasPriceProvider fallbackProvider = mock(MinGasPriceProvider.class); + when(fallbackProvider.getType()).thenReturn(MinGasPriceProviderType.FIXED); + + SimpleHttpClient httpClient = mock(SimpleHttpClient.class); + when(httpClient.doGet(anyString())).thenReturn("{\"bitcoin\":{\"usd\":10000}}"); + StableMinGasPriceSystemConfig config = createStableMinGasPriceSystemConfig(); + HttpGetMinGasPriceProvider provider = spy(new HttpGetMinGasPriceProvider(config, fallbackProvider, httpClient)); + + provider.getMinGasPrice(); + provider.getMinGasPrice(true); + + verify(provider, times(2)).getMinGasPrice(anyBoolean()); + verify(httpClient, times(1)).doGet(anyString()); + } + + @Test + void whenEmptyResponseReturnsFallbackProvider() throws HttpException { + MinGasPriceProvider fallbackProvider = mock(MinGasPriceProvider.class); + when(fallbackProvider.getMinGasPrice()).thenReturn(10L); + when(fallbackProvider.getType()).thenReturn(MinGasPriceProviderType.FIXED); + + SimpleHttpClient httpClient = mock(SimpleHttpClient.class); + when(httpClient.doGet(anyString())).thenReturn(""); + StableMinGasPriceSystemConfig config = createStableMinGasPriceSystemConfig(); + HttpGetMinGasPriceProvider provider = new HttpGetMinGasPriceProvider(config, fallbackProvider, httpClient); + + Long result = provider.getMinGasPrice(true); + verify(fallbackProvider, times(1)).getMinGasPrice(); + assertEquals(10L, result); + } + + @Test + void whenErrorReturnsFallbackProvider() throws HttpException { + MinGasPriceProvider fallbackProvider = mock(MinGasPriceProvider.class); + when(fallbackProvider.getMinGasPrice()).thenReturn(10L); + when(fallbackProvider.getType()).thenReturn(MinGasPriceProviderType.FIXED); + + SimpleHttpClient httpClient = mock(SimpleHttpClient.class); + when(httpClient.doGet(anyString())).thenThrow(new HttpException("Error")); + StableMinGasPriceSystemConfig config = createStableMinGasPriceSystemConfig(); + HttpGetMinGasPriceProvider provider = new HttpGetMinGasPriceProvider(config, fallbackProvider, httpClient); + + Long result = provider.getMinGasPrice(true); + verify(fallbackProvider, times(1)).getMinGasPrice(); + assertEquals(10L, result); + } + + @Test + void whenPathIsWrongReturnsFallBack() throws HttpException { + MinGasPriceProvider fallbackProvider = mock(MinGasPriceProvider.class); + when(fallbackProvider.getMinGasPrice()).thenReturn(10L); + when(fallbackProvider.getType()).thenReturn(MinGasPriceProviderType.FIXED); + + SimpleHttpClient httpClient = mock(SimpleHttpClient.class); + when(httpClient.doGet(anyString())).thenReturn("{\"btc\":{\"usd\":10000}}"); + StableMinGasPriceSystemConfig config = createStableMinGasPriceSystemConfig(); + HttpGetMinGasPriceProvider provider = new HttpGetMinGasPriceProvider(config, fallbackProvider, httpClient); + + Long result = provider.getMinGasPrice(true); + + verify(fallbackProvider, times(1)).getMinGasPrice(); + assertEquals(10L, result); + } + + private StableMinGasPriceSystemConfig createStableMinGasPriceSystemConfig() { + StableMinGasPriceSystemConfig config = mock(StableMinGasPriceSystemConfig.class); + HttpGetStableMinGasSystemConfig webConfig = createWebStableSystemConfig(); + when(config.getHttpGetConfig()).thenReturn(webConfig); + when(config.getMinStableGasPrice()).thenReturn(4265280000000L); + when(config.getRefreshRate()).thenReturn(Duration.ofSeconds(30)); + return config; + } + + private HttpGetStableMinGasSystemConfig createWebStableSystemConfig() { + HttpGetStableMinGasSystemConfig config = mock(HttpGetStableMinGasSystemConfig.class); + when(config.getJsonPath()).thenReturn("/bitcoin/usd"); + when(config.getUrl()).thenReturn("https://rsk.co/price?ids=bitcoin&vs_currencies=usd"); + when(config.getTimeout()).thenReturn(Duration.ofMillis(3000)); + return config; + } + +} \ No newline at end of file diff --git a/rskj-core/src/test/java/co/rsk/mine/gas/provider/MinGasPriceProviderFactoryTest.java b/rskj-core/src/test/java/co/rsk/mine/gas/provider/MinGasPriceProviderFactoryTest.java new file mode 100644 index 00000000000..96a85b8028f --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/mine/gas/provider/MinGasPriceProviderFactoryTest.java @@ -0,0 +1,96 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.mine.gas.provider; + +import co.rsk.config.mining.StableMinGasPriceSystemConfig; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MinGasPriceProviderFactoryTest { + + @Test + void createFixedMinGasPriceProvider() { + Config testConfig = ConfigFactory.parseString( + "enabled=true\n" + + "refreshRate=10\n" + + "minStableGasPrice=100\n" + + "source={ method=FIXED }" + ); + StableMinGasPriceSystemConfig config = new StableMinGasPriceSystemConfig(testConfig); + + MinGasPriceProvider provider = MinGasPriceProviderFactory.create(100L, config, () -> null); + + assertTrue(provider instanceof FixedMinGasPriceProvider); + assertEquals(100, provider.getMinGasPrice()); + assertEquals(MinGasPriceProviderType.FIXED, provider.getType()); + } + + @Test + void createWithNullConfig() { + MinGasPriceProvider provider = MinGasPriceProviderFactory.create(100L, null, () -> null); + assertTrue(provider instanceof FixedMinGasPriceProvider); + assertEquals(100, provider.getMinGasPrice()); + assertEquals(MinGasPriceProviderType.FIXED, provider.getType()); + } + + @Test + void createWebProviderMethod() { + Config testConfig = ConfigFactory.parseString( + "enabled=true\n" + + "refreshRate=10\n" + + "minStableGasPrice=100\n" + + "source.method=HTTP_GET\n" + + "source.params.url=url\n" + + "source.params.timeout=1000\n" + + "source.params.jsonPath=/price" + ); + + StableMinGasPriceSystemConfig disabledConfig = new StableMinGasPriceSystemConfig(testConfig); + + MinGasPriceProvider provider = MinGasPriceProviderFactory.create(100L, disabledConfig, () -> null); + + assertTrue(provider instanceof HttpGetMinGasPriceProvider); + assertEquals(100, provider.getMinGasPrice()); + assertEquals(MinGasPriceProviderType.HTTP_GET, provider.getType()); + } + + @Test + void createWithDisabledConfigReturnFixed() { + Config testConfig = ConfigFactory.parseString( + "enabled=false\n" + + "refreshRate=10\n" + + "minStableGasPrice=100\n" + + "source.method=HTTP_GET\n" + + "source.params.url=url\n" + + "source.params.timeout=1000 \n" + + "source.params.jsonPath=price" + ); + + StableMinGasPriceSystemConfig disabledConfig = new StableMinGasPriceSystemConfig(testConfig); + + MinGasPriceProvider provider = MinGasPriceProviderFactory.create(100L, disabledConfig, () -> null); + + assertTrue(provider instanceof FixedMinGasPriceProvider); + assertEquals(100, provider.getMinGasPrice()); + assertEquals(MinGasPriceProviderType.FIXED, provider.getType()); + } +} diff --git a/rskj-core/src/test/java/co/rsk/mine/gas/provider/StableMinGasPriceProviderTest.java b/rskj-core/src/test/java/co/rsk/mine/gas/provider/StableMinGasPriceProviderTest.java new file mode 100644 index 00000000000..5b4269f9faf --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/mine/gas/provider/StableMinGasPriceProviderTest.java @@ -0,0 +1,101 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.mine.gas.provider; + +import co.rsk.core.Coin; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.time.Duration; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +class StableMinGasPriceProviderTest { + private MinGasPriceProvider fallBackProvider; + private StableMinGasPriceProvider stableMinGasPriceProvider; + + @BeforeEach + void setUp() { + fallBackProvider = Mockito.mock(MinGasPriceProvider.class); + when(fallBackProvider.getMinGasPrice()).thenReturn(10L); + when(fallBackProvider.getType()).thenReturn(MinGasPriceProviderType.FIXED); + stableMinGasPriceProvider = new TestStableMingGasPriceProvider(fallBackProvider, 100, Duration.ofSeconds(10)); + } + + @Test + void testGetMinGasPrice() { + long result = stableMinGasPriceProvider.getMinGasPrice(true); + assertEquals(6L, result); + verify(fallBackProvider, times(0)).getMinGasPrice(); + } + + @Test + void GetMinGasPriceUsesFallbackWhenReturnIsNull() { + stableMinGasPriceProvider = new TestStableMingGasPriceProvider(fallBackProvider, 100, Duration.ofSeconds(10)) { + @Override + public Optional getBtcExchangeRate() { + return Optional.empty(); + } + }; + + long result = stableMinGasPriceProvider.getMinGasPrice(true); + + assertEquals(10L, result); + verify(fallBackProvider, times(1)).getMinGasPrice(); + } + + @Test + void whenRequestingTheValueTwiceCachedValueIsUsed() { + MinGasPriceProvider fallbackProvider = mock(MinGasPriceProvider.class); + when(fallbackProvider.getType()).thenReturn(MinGasPriceProviderType.FIXED); + + stableMinGasPriceProvider = spy(new TestStableMingGasPriceProvider(fallBackProvider, 100, Duration.ofSeconds(10))); + + stableMinGasPriceProvider.getMinGasPrice(); + stableMinGasPriceProvider.getMinGasPrice(true); + + verify(stableMinGasPriceProvider, times(2)).getMinGasPrice(anyBoolean()); + verify(stableMinGasPriceProvider, times(1)).getBtcExchangeRate(); + } + @Test + void testGetMinGasPriceAsCoin() { + Coin result = stableMinGasPriceProvider.getMinGasPriceAsCoin(true); + assertEquals(Coin.valueOf(6L), result); + } + + + public static class TestStableMingGasPriceProvider extends StableMinGasPriceProvider { + + protected TestStableMingGasPriceProvider(MinGasPriceProvider fallBackProvider, long minStableGasPrice, Duration refreshRate) { + super(fallBackProvider, minStableGasPrice, refreshRate); + } + + @Override + public MinGasPriceProviderType getType() { + return MinGasPriceProviderType.FIXED; + } + + @Override + public Optional getBtcExchangeRate() { + return Optional.of(15L); + } + } +} diff --git a/rskj-core/src/test/java/co/rsk/net/http/SimpleHttpClientIntTest.java b/rskj-core/src/test/java/co/rsk/net/http/SimpleHttpClientIntTest.java new file mode 100644 index 00000000000..1c4c1d6a426 --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/net/http/SimpleHttpClientIntTest.java @@ -0,0 +1,111 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.net.http; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Random; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Disabled("Use this class to check the behavior of the SimpleHttpClient class. Disabled by default to not run in CI.") +public class SimpleHttpClientIntTest { + private static SimpleHttpTestServer server; + private static String baseUrl; + private static int port; + + + @BeforeAll + public static void setUp() throws Exception { + port = generateRandomPort(); + server = new SimpleHttpTestServer(); + Thread serverThread = new Thread(new Runnable() { + @Override + public void run() { + try { + server.start(port); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + serverThread.setDaemon(true); + serverThread.start(); + // Give the server a second to ensure it's up and running + Thread.sleep(1000); + baseUrl = "http://localhost:" + port; + } + + @AfterAll + public static void tearDown() { + server.stop(); + } + + @Test + void testGetRequest() throws HttpException { + SimpleHttpClient client = new SimpleHttpClient(25000); // 5 seconds timeout + String response = client.doGet(baseUrl + SimpleHttpTestServer.HELLO_PATH); + assertEquals(SimpleHttpTestServer.HELLO_RESPONSE, response); + } + + @Test + void testNotFoundRequest() { + SimpleHttpClient client = new SimpleHttpClient(25000); + HttpException exception = assertThrows(HttpException.class, () -> { + client.doGet(baseUrl + SimpleHttpTestServer.NOT_FOUND_PATH); + }); + assertEquals("Http request failed with code : 404 - Not Found", exception.getMessage()); + } + + @Test + void testInternalServerError() { + SimpleHttpClient client = new SimpleHttpClient(25000); + HttpException exception = assertThrows(HttpException.class, () -> { + client.doGet(baseUrl + SimpleHttpTestServer.ERROR_PATH); + }); + assertEquals("Http request failed with code : 500 - Internal Server Error", exception.getMessage()); + } + + @Test + void testInvalidUrl() { + SimpleHttpClient client = new SimpleHttpClient(25000); + assertThrows(HttpException.class, () -> { + client.doGet("invalid_url"); + }); + } + + @Test + void testTimeout() { + SimpleHttpClient client = new SimpleHttpClient(1); // set timeout to 1 ms + assertThrows(HttpException.class, () -> { + client.doGet(baseUrl + SimpleHttpTestServer.SLOW_PATH); + }); + } + + private static int generateRandomPort() { + Random random = new Random(); + int minPort = 49152; + int maxPort = 65535; + return random.nextInt((maxPort - minPort) + 1) + minPort; + } +} diff --git a/rskj-core/src/test/java/co/rsk/net/http/SimpleHttpTestServer.java b/rskj-core/src/test/java/co/rsk/net/http/SimpleHttpTestServer.java new file mode 100644 index 00000000000..42140f780e4 --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/net/http/SimpleHttpTestServer.java @@ -0,0 +1,101 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.net.http; + +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpExchange; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; + +public class SimpleHttpTestServer { + + protected static final String HELLO_RESPONSE = "Hello, world!"; + protected static final String NOT_FOUND_RESPONSE = "Not Found"; + protected static final String ERROR_RESPONSE = "Internal Server Error"; + protected static final String SLOW_RESPONSE = "Slow Response"; + protected static final String HELLO_PATH = "/hello"; + protected static final String NOT_FOUND_PATH = "/notfound"; + protected static final String ERROR_PATH = "/error"; + protected static final String SLOW_PATH = "/slow"; private HttpServer server; + + public void start(int port) throws IOException { + server = HttpServer.create(new InetSocketAddress(port), 0); + server.createContext(HELLO_PATH, new HelloHandler()); + server.createContext(NOT_FOUND_PATH, new NotFoundHandler()); + server.createContext(ERROR_PATH, new ErrorHandler()); + server.createContext(SLOW_PATH, new SlowHandler()); + server.setExecutor(null); // creates a default executor + server.start(); + } + + public void stop() { + server.stop(0); + } + + static class HelloHandler implements HttpHandler { + @Override + public void handle(HttpExchange t) throws IOException { + String response = HELLO_RESPONSE; + t.sendResponseHeaders(200, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } + + static class NotFoundHandler implements HttpHandler { + @Override + public void handle(HttpExchange t) throws IOException { + String response = NOT_FOUND_RESPONSE; + t.sendResponseHeaders(404, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } + + static class ErrorHandler implements HttpHandler { + @Override + public void handle(HttpExchange t) throws IOException { + String response = ERROR_RESPONSE; + t.sendResponseHeaders(500, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } + + static class SlowHandler implements HttpHandler { + @Override + public void handle(HttpExchange t) throws IOException { + try { + Thread.sleep(5000); // sleep for 5 seconds to simulate slow response + } catch (InterruptedException e) { + e.printStackTrace(); + } + String response = SLOW_RESPONSE; + t.sendResponseHeaders(200, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } +} diff --git a/rskj-core/src/test/java/co/rsk/rpc/modules/trace/TraceModuleImplTest.java b/rskj-core/src/test/java/co/rsk/rpc/modules/trace/TraceModuleImplTest.java index 80a655a3e90..3268aefd13e 100644 --- a/rskj-core/src/test/java/co/rsk/rpc/modules/trace/TraceModuleImplTest.java +++ b/rskj-core/src/test/java/co/rsk/rpc/modules/trace/TraceModuleImplTest.java @@ -20,9 +20,9 @@ import co.rsk.config.GasLimitConfig; import co.rsk.config.MiningConfig; import co.rsk.config.RskSystemProperties; -import co.rsk.core.Coin; import co.rsk.core.DifficultyCalculator; import co.rsk.mine.*; +import co.rsk.mine.gas.provider.FixedMinGasPriceProvider; import co.rsk.rpc.ExecutionBlockRetriever; import co.rsk.test.World; import co.rsk.test.builders.AccountBuilder; @@ -34,7 +34,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.ethereum.core.*; +import org.ethereum.core.Account; +import org.ethereum.core.Block; +import org.ethereum.core.BlockFactory; +import org.ethereum.core.Transaction; import org.ethereum.datasource.HashMapDB; import org.ethereum.db.ReceiptStore; import org.ethereum.db.ReceiptStoreImpl; @@ -425,8 +428,7 @@ private static ExecutionBlockRetriever createExecutionBlockRetriever(World world rskSystemProperties.coinbaseAddress(), rskSystemProperties.minerMinFeesNotifyInDollars(), rskSystemProperties.minerGasUnitInDollars(), - rskSystemProperties.minerMinGasPrice(), - rskSystemProperties.getNetworkConstants().getUncleListLimit(), + rskSystemProperties.getNetworkConstants().getUncleListLimit(), rskSystemProperties.getNetworkConstants().getUncleGenerationLimit(), new GasLimitConfig( rskSystemProperties.getNetworkConstants().getMinGasLimit(), @@ -452,7 +454,7 @@ private static ExecutionBlockRetriever createExecutionBlockRetriever(World world new MinerClock(miningConfig.isFixedClock(), Clock.systemUTC()), new BlockFactory(rskSystemProperties.getActivationConfig()), world.getBlockExecutor(), - new MinimumGasPriceCalculator(Coin.valueOf(miningConfig.getMinGasPriceTarget())), + new MinimumGasPriceCalculator(new FixedMinGasPriceProvider(rskSystemProperties.minerMinGasPrice())), new MinerUtils(), world.getBlockTxSignatureCache() ); diff --git a/rskj-core/src/test/java/org/ethereum/rpc/Web3ImplSnapshotTest.java b/rskj-core/src/test/java/org/ethereum/rpc/Web3ImplSnapshotTest.java index 129731b242f..9084e7a08e9 100644 --- a/rskj-core/src/test/java/org/ethereum/rpc/Web3ImplSnapshotTest.java +++ b/rskj-core/src/test/java/org/ethereum/rpc/Web3ImplSnapshotTest.java @@ -21,12 +21,12 @@ import co.rsk.config.ConfigUtils; import co.rsk.config.MiningConfig; import co.rsk.config.TestSystemProperties; -import co.rsk.core.Coin; import co.rsk.core.DifficultyCalculator; import co.rsk.core.SnapshotManager; import co.rsk.core.bc.BlockChainStatus; import co.rsk.core.bc.MiningMainchainView; import co.rsk.mine.*; +import co.rsk.mine.gas.provider.FixedMinGasPriceProvider; import co.rsk.rpc.modules.debug.DebugModule; import co.rsk.rpc.modules.debug.DebugModuleImpl; import co.rsk.rpc.modules.evm.EvmModule; @@ -234,7 +234,7 @@ private MinerServer getMinerServerForTest(SimpleEthereum ethereum, MinerClock cl clock, blockFactory, factory.getBlockExecutor(), - new MinimumGasPriceCalculator(Coin.valueOf(miningConfig.getMinGasPriceTarget())), + new MinimumGasPriceCalculator(new FixedMinGasPriceProvider(config.minerMinGasPrice())), new MinerUtils(), new BlockTxSignatureCache(new ReceivedTxSignatureCache()) ),