From 636ad8a65a061d8a66686b5fb86de86958197308 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Wed, 8 Nov 2023 15:26:22 +0100 Subject: [PATCH] Force tx replacement price bump to zero when zero base fee market is configured (#6079) Signed-off-by: Fabio Di Fabio --- CHANGELOG.md | 2 +- .../org/hyperledger/besu/cli/BesuCommand.java | 37 ++-- .../{stable => }/TransactionPoolOptions.java | 68 ++++++- .../unstable/TransactionPoolOptions.java | 94 ---------- .../besu/cli/util/CommandLineUtils.java | 13 ++ .../hyperledger/besu/cli/BesuCommandTest.java | 53 ++++++ .../besu/cli/CommandTestAbstract.java | 11 +- .../TransactionPoolOptionsTest.java | 55 +++++- .../unstable/TransactionPoolOptionsTest.java | 105 ----------- ...TransactionReplacementByFeeMarketRule.java | 7 +- .../TransactionReplacementByGasPriceRule.java | 2 +- .../AbstractTransactionPoolTest.java | 175 ++++++++++++++++-- .../AbstractTransactionReplacementTest.java | 53 ++++++ ...sactionReplacementByFeeMarketRuleTest.java | 100 ++++++++++ ...nsactionReplacementByGasPriceRuleTest.java | 87 +++++++++ .../TransactionReplacementRulesTest.java | 56 ++---- .../LegacyTransactionPoolBaseFeeTest.java | 7 +- .../LegacyTransactionPoolGasPriceTest.java | 7 +- .../besu/util/number/Percentage.java | 2 + 19 files changed, 635 insertions(+), 299 deletions(-) rename besu/src/main/java/org/hyperledger/besu/cli/options/{stable => }/TransactionPoolOptions.java (81%) delete mode 100644 besu/src/main/java/org/hyperledger/besu/cli/options/unstable/TransactionPoolOptions.java rename besu/src/test/java/org/hyperledger/besu/cli/options/{stable => }/TransactionPoolOptionsTest.java (82%) delete mode 100644 besu/src/test/java/org/hyperledger/besu/cli/options/unstable/TransactionPoolOptionsTest.java create mode 100644 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/AbstractTransactionReplacementTest.java create mode 100644 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByFeeMarketRuleTest.java create mode 100644 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByGasPriceRuleTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index d3ee7a889c3..69ac2df1d42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,9 @@ - Clique config option `createemptyblocks` to not create empty blocks [#6082](https://github.com/hyperledger/besu/pull/6082) - Upgrade EVM Reference Tests to v13 (Cancun) [#6114](https://github.com/hyperledger/besu/pull/6114) - Add `yParity` to GraphQL and JSON-RPC for relevant querise. [6119](https://github.com/hyperledger/besu/pull/6119) +- Force tx replacement price bump to zero when zero base fee market is configured or `--min-gas-price` is set to 0. This allows for easier tx replacement in networks where there is not gas price. [#6079](https://github.com/hyperledger/besu/pull/6079) ### Bug fixes - - Upgrade netty to address CVE-2023-44487, CVE-2023-34462 [#6100](https://github.com/hyperledger/besu/pull/6100) - Upgrade grpc to address CVE-2023-32731, CVE-2023-33953, CVE-2023-44487, CVE-2023-4785 [#6100](https://github.com/hyperledger/besu/pull/6100) - Fix blob gas calculation in reference tests [#6107](https://github.com/hyperledger/besu/pull/6107) diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index 5a6fc386716..d1af3efe7f5 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -54,12 +54,12 @@ import org.hyperledger.besu.cli.error.BesuExecutionExceptionHandler; import org.hyperledger.besu.cli.error.BesuParameterExceptionHandler; import org.hyperledger.besu.cli.options.MiningOptions; +import org.hyperledger.besu.cli.options.TransactionPoolOptions; import org.hyperledger.besu.cli.options.stable.DataStorageOptions; import org.hyperledger.besu.cli.options.stable.EthstatsOptions; import org.hyperledger.besu.cli.options.stable.LoggingLevelOption; import org.hyperledger.besu.cli.options.stable.NodePrivateKeyFileOption; import org.hyperledger.besu.cli.options.stable.P2PTLSConfigOptions; -import org.hyperledger.besu.cli.options.stable.TransactionPoolOptions; import org.hyperledger.besu.cli.options.unstable.ChainPruningOptions; import org.hyperledger.besu.cli.options.unstable.DnsOptions; import org.hyperledger.besu.cli.options.unstable.EthProtocolOptions; @@ -283,9 +283,6 @@ public class BesuCommand implements DefaultCommandValues, Runnable { final SynchronizerOptions unstableSynchronizerOptions = SynchronizerOptions.create(); final EthProtocolOptions unstableEthProtocolOptions = EthProtocolOptions.create(); final MetricsCLIOptions unstableMetricsCLIOptions = MetricsCLIOptions.create(); - final org.hyperledger.besu.cli.options.unstable.TransactionPoolOptions - unstableTransactionPoolOptions = - org.hyperledger.besu.cli.options.unstable.TransactionPoolOptions.create(); private final DnsOptions unstableDnsOptions = DnsOptions.create(); private final NatOptions unstableNatOptions = NatOptions.create(); private final NativeLibraryOptions unstableNativeLibraryOptions = NativeLibraryOptions.create(); @@ -303,8 +300,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable { private final LoggingLevelOption loggingLevelOption = LoggingLevelOption.create(); @CommandLine.ArgGroup(validate = false, heading = "@|bold Tx Pool Common Options|@%n") - final org.hyperledger.besu.cli.options.stable.TransactionPoolOptions - stableTransactionPoolOptions = TransactionPoolOptions.create(); + final TransactionPoolOptions transactionPoolOptions = TransactionPoolOptions.create(); @CommandLine.ArgGroup(validate = false, heading = "@|bold Block Builder Options|@%n") final MiningOptions miningOptions = MiningOptions.create(); @@ -1525,7 +1521,6 @@ private void handleUnstableOptions() { .put("NAT Configuration", unstableNatOptions) .put("Privacy Plugin Configuration", unstablePrivacyPluginOptions) .put("Synchronizer", unstableSynchronizerOptions) - .put("TransactionPool", unstableTransactionPoolOptions) .put("Native Library", unstableNativeLibraryOptions) .put("EVM Options", unstableEvmOptions) .put("IPC Options", unstableIpcOptions) @@ -1794,7 +1789,7 @@ private void validateOptions() { } private void validateTransactionPoolOptions() { - stableTransactionPoolOptions.validate(commandLine); + transactionPoolOptions.validate(commandLine, getActualGenesisConfigOptions()); } private void validateRequiredOptions() { @@ -2811,12 +2806,26 @@ private SynchronizerConfiguration buildSyncConfig() { } private TransactionPoolConfiguration buildTransactionPoolConfiguration() { - final var stableTxPoolOption = stableTransactionPoolOptions.toDomainObject(); - return ImmutableTransactionPoolConfiguration.builder() - .from(stableTxPoolOption) - .unstable(unstableTransactionPoolOptions.toDomainObject()) - .saveFile((dataPath.resolve(stableTxPoolOption.getSaveFile().getPath()).toFile())) - .build(); + final var txPoolConf = transactionPoolOptions.toDomainObject(); + final var txPoolConfBuilder = + ImmutableTransactionPoolConfiguration.builder() + .from(txPoolConf) + .saveFile((dataPath.resolve(txPoolConf.getSaveFile().getPath()).toFile())); + + if (getActualGenesisConfigOptions().isZeroBaseFee()) { + logger.info( + "Forcing price bump for transaction replacement to 0, since we are on a zero basefee network"); + txPoolConfBuilder.priceBump(Percentage.ZERO); + } + + if (getMiningParameters().getMinTransactionGasPrice().equals(Wei.ZERO) + && !transactionPoolOptions.isPriceBumpSet(commandLine)) { + logger.info( + "Forcing price bump for transaction replacement to 0, since min-gas-price is set to 0"); + txPoolConfBuilder.priceBump(Percentage.ZERO); + } + + return txPoolConfBuilder.build(); } private MiningParameters getMiningParameters() { diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/stable/TransactionPoolOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/TransactionPoolOptions.java similarity index 81% rename from besu/src/main/java/org/hyperledger/besu/cli/options/stable/TransactionPoolOptions.java rename to besu/src/main/java/org/hyperledger/besu/cli/options/TransactionPoolOptions.java index d481e0453a0..a8a50025f58 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/options/stable/TransactionPoolOptions.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/options/TransactionPoolOptions.java @@ -12,7 +12,7 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package org.hyperledger.besu.cli.options.stable; +package org.hyperledger.besu.cli.options; import static org.hyperledger.besu.cli.DefaultCommandValues.MANDATORY_DOUBLE_FORMAT_HELP; import static org.hyperledger.besu.cli.DefaultCommandValues.MANDATORY_INTEGER_FORMAT_HELP; @@ -20,10 +20,11 @@ import static org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration.Implementation.LAYERED; import static org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration.Implementation.LEGACY; +import org.hyperledger.besu.cli.converter.DurationMillisConverter; import org.hyperledger.besu.cli.converter.FractionConverter; import org.hyperledger.besu.cli.converter.PercentageConverter; -import org.hyperledger.besu.cli.options.CLIOptions; import org.hyperledger.besu.cli.util.CommandLineUtils; +import org.hyperledger.besu.config.GenesisConfigOptions; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; @@ -32,6 +33,7 @@ import org.hyperledger.besu.util.number.Percentage; import java.io.File; +import java.time.Duration; import java.util.List; import java.util.Set; @@ -195,6 +197,39 @@ static class Legacy { Integer txPoolMaxSize = TransactionPoolConfiguration.DEFAULT_MAX_PENDING_TRANSACTIONS; } + @CommandLine.ArgGroup(validate = false) + private final TransactionPoolOptions.Unstable unstableOptions = + new TransactionPoolOptions.Unstable(); + + static class Unstable { + private static final String TX_MESSAGE_KEEP_ALIVE_SEC_FLAG = + "--Xincoming-tx-messages-keep-alive-seconds"; + + private static final String ETH65_TX_ANNOUNCED_BUFFERING_PERIOD_FLAG = + "--Xeth65-tx-announced-buffering-period-milliseconds"; + + @CommandLine.Option( + names = {TX_MESSAGE_KEEP_ALIVE_SEC_FLAG}, + paramLabel = "", + hidden = true, + description = + "Keep alive of incoming transaction messages in seconds (default: ${DEFAULT-VALUE})", + arity = "1") + private Integer txMessageKeepAliveSeconds = + TransactionPoolConfiguration.Unstable.DEFAULT_TX_MSG_KEEP_ALIVE; + + @CommandLine.Option( + names = {ETH65_TX_ANNOUNCED_BUFFERING_PERIOD_FLAG}, + paramLabel = "", + converter = DurationMillisConverter.class, + hidden = true, + description = + "The period for which the announced transactions remain in the buffer before being requested from the peers in milliseconds (default: ${DEFAULT-VALUE})", + arity = "1") + private Duration eth65TrxAnnouncedBufferingPeriod = + TransactionPoolConfiguration.Unstable.ETH65_TRX_ANNOUNCED_BUFFERING_PERIOD; + } + private TransactionPoolOptions() {} /** @@ -230,6 +265,10 @@ public static TransactionPoolOptions fromConfig(final TransactionPoolConfigurati config.getTxPoolLimitByAccountPercentage(); options.legacyOptions.txPoolMaxSize = config.getTxPoolMaxSize(); options.legacyOptions.pendingTxRetentionPeriod = config.getPendingTxRetentionPeriod(); + options.unstableOptions.txMessageKeepAliveSeconds = + config.getUnstable().getTxMessageKeepAliveSeconds(); + options.unstableOptions.eth65TrxAnnouncedBufferingPeriod = + config.getUnstable().getEth65TrxAnnouncedBufferingPeriod(); return options; } @@ -239,8 +278,10 @@ public static TransactionPoolOptions fromConfig(final TransactionPoolConfigurati * options are valid for the selected implementation. * * @param commandLine the full commandLine to check all the options specified by the user + * @param genesisConfigOptions the genesis config options */ - public void validate(final CommandLine commandLine) { + public void validate( + final CommandLine commandLine, final GenesisConfigOptions genesisConfigOptions) { CommandLineUtils.failIfOptionDoesntMeetRequirement( commandLine, "Could not use legacy transaction pool options with layered implementation", @@ -252,6 +293,12 @@ public void validate(final CommandLine commandLine) { "Could not use layered transaction pool options with legacy implementation", !txPoolImplementation.equals(LEGACY), CommandLineUtils.getCLIOptionNames(Layered.class)); + + CommandLineUtils.failIfOptionDoesntMeetRequirement( + commandLine, + "Price bump option is not compatible with zero base fee market", + !genesisConfigOptions.isZeroBaseFee(), + List.of(TX_POOL_PRICE_BUMP)); } @Override @@ -271,6 +318,11 @@ public TransactionPoolConfiguration toDomainObject() { .txPoolLimitByAccountPercentage(legacyOptions.txPoolLimitByAccountPercentage) .txPoolMaxSize(legacyOptions.txPoolMaxSize) .pendingTxRetentionPeriod(legacyOptions.pendingTxRetentionPeriod) + .unstable( + ImmutableTransactionPoolConfiguration.Unstable.builder() + .txMessageKeepAliveSeconds(unstableOptions.txMessageKeepAliveSeconds) + .eth65TrxAnnouncedBufferingPeriod(unstableOptions.eth65TrxAnnouncedBufferingPeriod) + .build()) .build(); } @@ -278,4 +330,14 @@ public TransactionPoolConfiguration toDomainObject() { public List getCLIOptions() { return CommandLineUtils.getCLIOptions(this, new TransactionPoolOptions()); } + + /** + * Is price bump option set? + * + * @param commandLine the command line + * @return true is tx-pool-price-bump is set + */ + public boolean isPriceBumpSet(final CommandLine commandLine) { + return CommandLineUtils.isOptionSet(commandLine, TransactionPoolOptions.TX_POOL_PRICE_BUMP); + } } diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/unstable/TransactionPoolOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/unstable/TransactionPoolOptions.java deleted file mode 100644 index cb7994b947f..00000000000 --- a/besu/src/main/java/org/hyperledger/besu/cli/options/unstable/TransactionPoolOptions.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright ConsenSys AG. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.hyperledger.besu.cli.options.unstable; - -import org.hyperledger.besu.cli.converter.DurationMillisConverter; -import org.hyperledger.besu.cli.options.CLIOptions; -import org.hyperledger.besu.cli.util.CommandLineUtils; -import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; -import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; - -import java.time.Duration; -import java.util.List; - -import picocli.CommandLine; - -/** The Transaction pool Cli unstable options. */ -public class TransactionPoolOptions implements CLIOptions { - private static final String TX_MESSAGE_KEEP_ALIVE_SEC_FLAG = - "--Xincoming-tx-messages-keep-alive-seconds"; - - private static final String ETH65_TX_ANNOUNCED_BUFFERING_PERIOD_FLAG = - "--Xeth65-tx-announced-buffering-period-milliseconds"; - - @CommandLine.Option( - names = {TX_MESSAGE_KEEP_ALIVE_SEC_FLAG}, - paramLabel = "", - hidden = true, - description = - "Keep alive of incoming transaction messages in seconds (default: ${DEFAULT-VALUE})", - arity = "1") - private Integer txMessageKeepAliveSeconds = - TransactionPoolConfiguration.Unstable.DEFAULT_TX_MSG_KEEP_ALIVE; - - @CommandLine.Option( - names = {ETH65_TX_ANNOUNCED_BUFFERING_PERIOD_FLAG}, - paramLabel = "", - converter = DurationMillisConverter.class, - hidden = true, - description = - "The period for which the announced transactions remain in the buffer before being requested from the peers in milliseconds (default: ${DEFAULT-VALUE})", - arity = "1") - private Duration eth65TrxAnnouncedBufferingPeriod = - TransactionPoolConfiguration.Unstable.ETH65_TRX_ANNOUNCED_BUFFERING_PERIOD; - - private TransactionPoolOptions() {} - - /** - * Create transaction pool options. - * - * @return the transaction pool options - */ - public static TransactionPoolOptions create() { - return new TransactionPoolOptions(); - } - - /** - * Create Transaction Pool Options from Transaction Pool Configuration. - * - * @param config the Transaction Pool Configuration - * @return the transaction pool options - */ - public static TransactionPoolOptions fromConfig( - final TransactionPoolConfiguration.Unstable config) { - final TransactionPoolOptions options = TransactionPoolOptions.create(); - options.txMessageKeepAliveSeconds = config.getTxMessageKeepAliveSeconds(); - options.eth65TrxAnnouncedBufferingPeriod = config.getEth65TrxAnnouncedBufferingPeriod(); - return options; - } - - @Override - public TransactionPoolConfiguration.Unstable toDomainObject() { - return ImmutableTransactionPoolConfiguration.Unstable.builder() - .txMessageKeepAliveSeconds(txMessageKeepAliveSeconds) - .eth65TrxAnnouncedBufferingPeriod(eth65TrxAnnouncedBufferingPeriod) - .build(); - } - - @Override - public List getCLIOptions() { - return CommandLineUtils.getCLIOptions(this, new TransactionPoolOptions()); - } -} diff --git a/besu/src/main/java/org/hyperledger/besu/cli/util/CommandLineUtils.java b/besu/src/main/java/org/hyperledger/besu/cli/util/CommandLineUtils.java index 33b531b5903..397592459c6 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/util/CommandLineUtils.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/util/CommandLineUtils.java @@ -248,4 +248,17 @@ private static boolean isOptionSet(final CommandLine.Model.OptionSpec option) { return false; } } + + /** + * Is the option with that name set on the command line? + * + * @param commandLine the command line + * @param optionName the option name to check + * @return true if set + */ + public static boolean isOptionSet(final CommandLine commandLine, final String optionName) { + return commandLine.getCommandSpec().options().stream() + .filter(optionSpec -> Arrays.stream(optionSpec.names()).anyMatch(optionName::equals)) + .anyMatch(CommandLineUtils::isOptionSet); + } } diff --git a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java index 91a203ff576..cfd90bc42e1 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java @@ -183,6 +183,9 @@ public class BesuCommandTest extends CommandTestAbstract { private static final JsonObject GENESIS_WITH_DATA_BLOBS_ENABLED = new JsonObject().put("config", new JsonObject().put("cancunTime", 1L)); + private static final JsonObject GENESIS_WITH_ZERO_BASE_FEE_MARKET = + new JsonObject().put("config", new JsonObject().put("zeroBaseFee", true)); + static { DEFAULT_JSON_RPC_CONFIGURATION = JsonRpcConfiguration.createDefault(); DEFAULT_GRAPH_QL_CONFIGURATION = GraphQLConfiguration.createDefault(); @@ -5195,6 +5198,56 @@ public void txpoolSaveFileAbsolutePathOutsideDataPath() throws IOException { assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); } + @Test + public void txpoolForcePriceBumpToZeroWhenZeroBaseFeeMarket() throws IOException { + final Path genesisFile = createFakeGenesisFile(GENESIS_WITH_ZERO_BASE_FEE_MARKET); + parseCommand("--genesis-file", genesisFile.toString()); + verify(mockControllerBuilder) + .transactionPoolConfiguration(transactionPoolConfigCaptor.capture()); + + final Percentage priceBump = transactionPoolConfigCaptor.getValue().getPriceBump(); + assertThat(priceBump).isEqualTo(Percentage.ZERO); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void txpoolPriceBumpOptionIncompatibleWithZeroWhenZeroBaseFeeMarket() throws IOException { + final Path genesisFile = createFakeGenesisFile(GENESIS_WITH_ZERO_BASE_FEE_MARKET); + parseCommand("--genesis-file", genesisFile.toString(), "--tx-pool-price-bump", "5"); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)) + .contains("Price bump option is not compatible with zero base fee market"); + } + + @Test + public void txpoolForcePriceBumpToZeroWhenMinGasPriceZero() { + parseCommand("--min-gas-price", "0"); + verify(mockControllerBuilder) + .transactionPoolConfiguration(transactionPoolConfigCaptor.capture()); + + final Percentage priceBump = transactionPoolConfigCaptor.getValue().getPriceBump(); + assertThat(priceBump).isEqualTo(Percentage.ZERO); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + + @Test + public void txpoolPriceBumpKeepItsValueIfSetEvenWhenMinGasPriceZero() { + parseCommand("--min-gas-price", "0", "--tx-pool-price-bump", "1"); + verify(mockControllerBuilder) + .transactionPoolConfiguration(transactionPoolConfigCaptor.capture()); + + final Percentage priceBump = transactionPoolConfigCaptor.getValue().getPriceBump(); + assertThat(priceBump).isEqualTo(Percentage.fromInt(1)); + + assertThat(commandOutput.toString(UTF_8)).isEmpty(); + assertThat(commandErrorOutput.toString(UTF_8)).isEmpty(); + } + @Test public void snapsyncHealingOptionShouldBeDisabledByDefault() { final TestBesuCommand besuCommand = parseCommand(); diff --git a/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java b/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java index bccf28c4522..a2c793296dd 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java @@ -32,12 +32,12 @@ import org.hyperledger.besu.chainimport.RlpBlockImporter; import org.hyperledger.besu.cli.config.EthNetworkConfig; import org.hyperledger.besu.cli.options.MiningOptions; +import org.hyperledger.besu.cli.options.TransactionPoolOptions; import org.hyperledger.besu.cli.options.stable.EthstatsOptions; import org.hyperledger.besu.cli.options.unstable.EthProtocolOptions; import org.hyperledger.besu.cli.options.unstable.MetricsCLIOptions; import org.hyperledger.besu.cli.options.unstable.NetworkingOptions; import org.hyperledger.besu.cli.options.unstable.SynchronizerOptions; -import org.hyperledger.besu.cli.options.unstable.TransactionPoolOptions; import org.hyperledger.besu.components.BesuComponent; import org.hyperledger.besu.consensus.qbft.pki.PkiBlockCreationConfiguration; import org.hyperledger.besu.consensus.qbft.pki.PkiBlockCreationConfigurationProvider; @@ -560,17 +560,12 @@ public EthProtocolOptions getEthProtocolOptions() { return unstableEthProtocolOptions; } - public org.hyperledger.besu.cli.options.stable.TransactionPoolOptions - getStableTransactionPoolOptions() { - return stableTransactionPoolOptions; - } - public MiningOptions getMiningOptions() { return miningOptions; } - public TransactionPoolOptions getUnstableTransactionPoolOptions() { - return unstableTransactionPoolOptions; + public TransactionPoolOptions getTransactionPoolOptions() { + return transactionPoolOptions; } public MetricsCLIOptions getMetricsCLIOptions() { diff --git a/besu/src/test/java/org/hyperledger/besu/cli/options/stable/TransactionPoolOptionsTest.java b/besu/src/test/java/org/hyperledger/besu/cli/options/TransactionPoolOptionsTest.java similarity index 82% rename from besu/src/test/java/org/hyperledger/besu/cli/options/stable/TransactionPoolOptionsTest.java rename to besu/src/test/java/org/hyperledger/besu/cli/options/TransactionPoolOptionsTest.java index e7977a921e1..5b5fff5014e 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/options/stable/TransactionPoolOptionsTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/options/TransactionPoolOptionsTest.java @@ -12,20 +12,21 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package org.hyperledger.besu.cli.options.stable; +package org.hyperledger.besu.cli.options; import static org.assertj.core.api.Assertions.assertThat; import static org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration.Implementation.LAYERED; import static org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration.Implementation.LEGACY; -import org.hyperledger.besu.cli.options.AbstractCLIOptionsTest; -import org.hyperledger.besu.cli.options.OptionParser; +import org.hyperledger.besu.cli.converter.DurationMillisConverter; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; import org.hyperledger.besu.util.number.Percentage; +import java.time.Duration; + import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @@ -270,6 +271,52 @@ public void malformedListOfPrioritySenders() { + prioritySender3.toHexString()); } + @Test + public void txMessageKeepAliveSeconds() { + final int txMessageKeepAliveSeconds = 999; + internalTestSuccess( + config -> + assertThat(config.getUnstable().getTxMessageKeepAliveSeconds()) + .isEqualTo(txMessageKeepAliveSeconds), + "--Xincoming-tx-messages-keep-alive-seconds", + String.valueOf(txMessageKeepAliveSeconds)); + } + + @Test + public void txMessageKeepAliveSecondsWithInvalidInputShouldFail() { + internalTestFailure( + "Invalid value for option '--Xincoming-tx-messages-keep-alive-seconds': 'acbd' is not an int", + "--Xincoming-tx-messages-keep-alive-seconds", + "acbd"); + } + + @Test + public void eth65TrxAnnouncedBufferingPeriod() { + final Duration eth65TrxAnnouncedBufferingPeriod = Duration.ofMillis(999); + internalTestSuccess( + config -> + assertThat(config.getUnstable().getEth65TrxAnnouncedBufferingPeriod()) + .isEqualTo(eth65TrxAnnouncedBufferingPeriod), + "--Xeth65-tx-announced-buffering-period-milliseconds", + new DurationMillisConverter().format(eth65TrxAnnouncedBufferingPeriod)); + } + + @Test + public void eth65TrxAnnouncedBufferingPeriodWithInvalidInputShouldFail() { + internalTestFailure( + "Invalid value for option '--Xeth65-tx-announced-buffering-period-milliseconds': cannot convert 'acbd' to Duration (org.hyperledger.besu.cli.converter.exception.DurationConversionException: 'acbd' is not a long)", + "--Xeth65-tx-announced-buffering-period-milliseconds", + "acbd"); + } + + @Test + public void eth65TrxAnnouncedBufferingPeriodWithInvalidInputShouldFail2() { + internalTestFailure( + "Invalid value for option '--Xeth65-tx-announced-buffering-period-milliseconds': cannot convert '-1' to Duration (org.hyperledger.besu.cli.converter.exception.DurationConversionException: negative value '-1' is not allowed)", + "--Xeth65-tx-announced-buffering-period-milliseconds", + "-1"); + } + @Override protected TransactionPoolConfiguration createDefaultDomainObject() { return TransactionPoolConfiguration.DEFAULT; @@ -294,6 +341,6 @@ protected TransactionPoolOptions optionsFromDomainObject( @Override protected TransactionPoolOptions getOptionsFromBesuCommand(final TestBesuCommand besuCommand) { - return besuCommand.getStableTransactionPoolOptions(); + return besuCommand.getTransactionPoolOptions(); } } diff --git a/besu/src/test/java/org/hyperledger/besu/cli/options/unstable/TransactionPoolOptionsTest.java b/besu/src/test/java/org/hyperledger/besu/cli/options/unstable/TransactionPoolOptionsTest.java deleted file mode 100644 index 4fc44f05167..00000000000 --- a/besu/src/test/java/org/hyperledger/besu/cli/options/unstable/TransactionPoolOptionsTest.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Hyperledger Besu Contributors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ -package org.hyperledger.besu.cli.options.unstable; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.hyperledger.besu.cli.converter.DurationMillisConverter; -import org.hyperledger.besu.cli.options.AbstractCLIOptionsTest; -import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; -import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; - -import java.time.Duration; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class TransactionPoolOptionsTest - extends AbstractCLIOptionsTest { - - @Test - public void txMessageKeepAliveSeconds() { - final int txMessageKeepAliveSeconds = 999; - internalTestSuccess( - config -> - assertThat(config.getTxMessageKeepAliveSeconds()).isEqualTo(txMessageKeepAliveSeconds), - "--Xincoming-tx-messages-keep-alive-seconds", - String.valueOf(txMessageKeepAliveSeconds)); - } - - @Test - public void txMessageKeepAliveSecondsWithInvalidInputShouldFail() { - internalTestFailure( - "Invalid value for option '--Xincoming-tx-messages-keep-alive-seconds': 'acbd' is not an int", - "--Xincoming-tx-messages-keep-alive-seconds", - "acbd"); - } - - @Test - public void eth65TrxAnnouncedBufferingPeriod() { - final Duration eth65TrxAnnouncedBufferingPeriod = Duration.ofMillis(999); - internalTestSuccess( - config -> - assertThat(config.getEth65TrxAnnouncedBufferingPeriod()) - .isEqualTo(eth65TrxAnnouncedBufferingPeriod), - "--Xeth65-tx-announced-buffering-period-milliseconds", - new DurationMillisConverter().format(eth65TrxAnnouncedBufferingPeriod)); - } - - @Test - public void eth65TrxAnnouncedBufferingPeriodWithInvalidInputShouldFail() { - internalTestFailure( - "Invalid value for option '--Xeth65-tx-announced-buffering-period-milliseconds': cannot convert 'acbd' to Duration (org.hyperledger.besu.cli.converter.exception.DurationConversionException: 'acbd' is not a long)", - "--Xeth65-tx-announced-buffering-period-milliseconds", - "acbd"); - } - - @Test - public void eth65TrxAnnouncedBufferingPeriodWithInvalidInputShouldFail2() { - internalTestFailure( - "Invalid value for option '--Xeth65-tx-announced-buffering-period-milliseconds': cannot convert '-1' to Duration (org.hyperledger.besu.cli.converter.exception.DurationConversionException: negative value '-1' is not allowed)", - "--Xeth65-tx-announced-buffering-period-milliseconds", - "-1"); - } - - @Override - protected TransactionPoolConfiguration.Unstable createDefaultDomainObject() { - return TransactionPoolConfiguration.Unstable.DEFAULT; - } - - @Override - protected TransactionPoolConfiguration.Unstable createCustomizedDomainObject() { - return ImmutableTransactionPoolConfiguration.Unstable.builder() - .txMessageKeepAliveSeconds( - TransactionPoolConfiguration.Unstable.DEFAULT_TX_MSG_KEEP_ALIVE + 1) - .eth65TrxAnnouncedBufferingPeriod( - TransactionPoolConfiguration.Unstable.ETH65_TRX_ANNOUNCED_BUFFERING_PERIOD.plus( - Duration.ofMillis(100))) - .build(); - } - - @Override - protected TransactionPoolOptions optionsFromDomainObject( - final TransactionPoolConfiguration.Unstable domainObject) { - return TransactionPoolOptions.fromConfig(domainObject); - } - - @Override - protected TransactionPoolOptions getOptionsFromBesuCommand(final TestBesuCommand besuCommand) { - return besuCommand.getUnstableTransactionPoolOptions(); - } -} diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByFeeMarketRule.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByFeeMarketRule.java index e89cc4600cd..559d6f51420 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByFeeMarketRule.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByFeeMarketRule.java @@ -49,11 +49,6 @@ public boolean shouldReplace( Wei newEffPriority = newPendingTransaction.getTransaction().getEffectivePriorityFeePerGas(maybeBaseFee); - // bail early if price is not strictly positive - if (newEffPrice.equals(Wei.ZERO)) { - return false; - } - Wei curEffPrice = priceOf(existingPendingTransaction.getTransaction(), maybeBaseFee); Wei curEffPriority = existingPendingTransaction.getTransaction().getEffectivePriorityFeePerGas(maybeBaseFee); @@ -77,6 +72,6 @@ private Wei priceOf(final Transaction transaction, final Optional maybeBase } private boolean isBumpedBy(final Wei val, final Wei bumpVal, final Percentage percent) { - return val.multiply(percent.getValue() + 100L).compareTo(bumpVal.multiply(100L)) < 0; + return val.multiply(percent.getValue() + 100L).compareTo(bumpVal.multiply(100L)) <= 0; } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByGasPriceRule.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByGasPriceRule.java index 541980aefcf..c94caf097d7 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByGasPriceRule.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByGasPriceRule.java @@ -41,6 +41,6 @@ public boolean shouldReplace( final Wei replacementThreshold = existingPendingTransaction.getGasPrice().multiply(100 + priceBump.getValue()).divide(100); - return newPendingTransaction.getGasPrice().compareTo(replacementThreshold) > 0; + return newPendingTransaction.getGasPrice().compareTo(replacementThreshold) >= 0; } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/AbstractTransactionPoolTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/AbstractTransactionPoolTest.java index ee3b6da2425..99121a2b8d1 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/AbstractTransactionPoolTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/AbstractTransactionPoolTest.java @@ -19,7 +19,6 @@ import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; import static org.hyperledger.besu.ethereum.mainnet.ValidationResult.valid; import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.EXCEEDS_BLOCK_GAS_LIMIT; import static org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason.GAS_PRICE_TOO_LOW; @@ -91,6 +90,7 @@ import org.hyperledger.besu.plugin.services.MetricsSystem; import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionValidator; import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionValidatorFactory; +import org.hyperledger.besu.util.number.Percentage; import java.math.BigInteger; import java.util.Arrays; @@ -106,6 +106,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; +import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; @@ -1033,6 +1034,157 @@ public void shouldAcceptZeroGasPrice1559TxsWhenMinGasPriceIsZeroAndLondonWithZer addAndAssertTransactionViaApiValid(transaction, noLocalPriority); } + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void samePriceTxReplacementWhenPriceBumpIsZeroFrontier(final boolean noLocalPriority) { + transactionPool = + createTransactionPool(b -> b.priceBump(Percentage.ZERO).noLocalPriority(noLocalPriority)); + when(miningParameters.getMinTransactionGasPrice()).thenReturn(Wei.ZERO); + + final Transaction transaction1a = + createBaseTransactionGasPriceMarket(0) + .gasPrice(Wei.ZERO) + .to(Optional.of(Address.ALTBN128_ADD)) + .createTransaction(KEY_PAIR1); + + givenTransactionIsValid(transaction1a); + + transactionPool.addRemoteTransactions(List.of(transaction1a)); + + assertThat(transactionPool.getPendingTransactions()) + .map(PendingTransaction::getTransaction) + .containsOnly(transaction1a); + + final Transaction transaction1b = + createBaseTransactionGasPriceMarket(0) + .gasPrice(Wei.ZERO) + .to(Optional.of(Address.KZG_POINT_EVAL)) + .createTransaction(KEY_PAIR1); + + givenTransactionIsValid(transaction1b); + + transactionPool.addRemoteTransactions(List.of(transaction1b)); + + assertThat(transactionPool.getPendingTransactions()) + .map(PendingTransaction::getTransaction) + .containsOnly(transaction1b); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + @EnabledIf("isBaseFeeMarket") + public void replaceSamePriceTxWhenPriceBumpIsZeroLondon(final boolean noLocalPriority) { + transactionPool = + createTransactionPool(b -> b.priceBump(Percentage.ZERO).noLocalPriority(noLocalPriority)); + when(miningParameters.getMinTransactionGasPrice()).thenReturn(Wei.ZERO); + + final Transaction transaction1a = + createBaseTransactionBaseFeeMarket(0) + .maxFeePerGas(Optional.of(Wei.ZERO)) + .maxPriorityFeePerGas(Optional.of(Wei.ZERO)) + .to(Optional.of(Address.ALTBN128_ADD)) + .createTransaction(KEY_PAIR1); + + givenTransactionIsValid(transaction1a); + + transactionPool.addRemoteTransactions(List.of(transaction1a)); + + assertThat(transactionPool.getPendingTransactions()) + .map(PendingTransaction::getTransaction) + .containsOnly(transaction1a); + + final Transaction transaction1b = + createBaseTransactionBaseFeeMarket(0) + .maxFeePerGas(Optional.of(Wei.ZERO)) + .maxPriorityFeePerGas(Optional.of(Wei.ZERO)) + .to(Optional.of(Address.KZG_POINT_EVAL)) + .createTransaction(KEY_PAIR1); + + givenTransactionIsValid(transaction1b); + + transactionPool.addRemoteTransactions(List.of(transaction1b)); + + assertThat(transactionPool.getPendingTransactions()) + .map(PendingTransaction::getTransaction) + .containsOnly(transaction1b); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + @EnabledIf("isBaseFeeMarket") + public void replaceSamePriceTxWhenPriceBumpIsZeroLondonToFrontier(final boolean noLocalPriority) { + transactionPool = + createTransactionPool(b -> b.priceBump(Percentage.ZERO).noLocalPriority(noLocalPriority)); + when(miningParameters.getMinTransactionGasPrice()).thenReturn(Wei.ZERO); + + final Transaction transaction1a = + createBaseTransactionBaseFeeMarket(0) + .maxFeePerGas(Optional.of(Wei.ZERO)) + .maxPriorityFeePerGas(Optional.of(Wei.ZERO)) + .to(Optional.of(Address.ALTBN128_ADD)) + .createTransaction(KEY_PAIR1); + + givenTransactionIsValid(transaction1a); + + transactionPool.addRemoteTransactions(List.of(transaction1a)); + + assertThat(transactionPool.getPendingTransactions()) + .map(PendingTransaction::getTransaction) + .containsOnly(transaction1a); + + final Transaction transaction1b = + createBaseTransactionGasPriceMarket(0) + .gasPrice(Wei.ZERO) + .to(Optional.of(Address.KZG_POINT_EVAL)) + .createTransaction(KEY_PAIR1); + + givenTransactionIsValid(transaction1b); + + transactionPool.addRemoteTransactions(List.of(transaction1b)); + + assertThat(transactionPool.getPendingTransactions()) + .map(PendingTransaction::getTransaction) + .containsOnly(transaction1b); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + @EnabledIf("isBaseFeeMarket") + public void replaceSamePriceTxWhenPriceBumpIsZeroFrontierToLondon(final boolean noLocalPriority) { + transactionPool = + createTransactionPool(b -> b.priceBump(Percentage.ZERO).noLocalPriority(noLocalPriority)); + when(miningParameters.getMinTransactionGasPrice()).thenReturn(Wei.ZERO); + + final Transaction transaction1a = + createBaseTransactionGasPriceMarket(0) + .gasPrice(Wei.ZERO) + .to(Optional.of(Address.KZG_POINT_EVAL)) + .createTransaction(KEY_PAIR1); + + givenTransactionIsValid(transaction1a); + + transactionPool.addRemoteTransactions(List.of(transaction1a)); + + assertThat(transactionPool.getPendingTransactions()) + .map(PendingTransaction::getTransaction) + .containsOnly(transaction1a); + + final Transaction transaction1b = + createBaseTransactionBaseFeeMarket(0) + .maxFeePerGas(Optional.of(Wei.ZERO)) + .maxPriorityFeePerGas(Optional.of(Wei.ZERO)) + .to(Optional.of(Address.ALTBN128_ADD)) + .createTransaction(KEY_PAIR1); + + givenTransactionIsValid(transaction1b); + + transactionPool.addRemoteTransactions(List.of(transaction1b)); + + assertThat(transactionPool.getPendingTransactions()) + .map(PendingTransaction::getTransaction) + .containsOnly(transaction1b); + } + @Test public void shouldAcceptBaseFeeFloorGasPriceFrontierLocalPriorityTransactionsWhenMining() { transactionPool = createTransactionPool(b -> b.noLocalPriority(false)); @@ -1112,20 +1264,21 @@ public void shouldAcceptLocal1559TxsWhenMaxFeePerGasIsAtLeastEqualToMinMinGasPri @Test public void addRemoteTransactionsShouldAllowDuplicates() { final Transaction transaction1 = createTransaction(1, Wei.of(7L)); - final Transaction transaction2 = createTransaction(2, Wei.of(7L)); - final Transaction transaction3 = createTransaction(2, Wei.of(7L)); - final Transaction transaction4 = createTransaction(3, Wei.of(7L)); + final Transaction transaction2a = createTransaction(2, Wei.of(7L)); + final Transaction transaction2b = createTransaction(2, Wei.of(7L)); + final Transaction transaction3 = createTransaction(3, Wei.of(7L)); givenTransactionIsValid(transaction1); - givenTransactionIsValid(transaction2); + givenTransactionIsValid(transaction2a); + givenTransactionIsValid(transaction2b); givenTransactionIsValid(transaction3); - givenTransactionIsValid(transaction4); - assertThatCode( - () -> - transactionPool.addRemoteTransactions( - List.of(transaction1, transaction2, transaction3, transaction4))) - .doesNotThrowAnyException(); + transactionPool.addRemoteTransactions( + List.of(transaction1, transaction2a, transaction2b, transaction3)); + + assertThat(transactionPool.getPendingTransactions()) + .map(PendingTransaction::getTransaction) + .containsExactlyInAnyOrder(transaction1, transaction2a, transaction3); } private static PluginTransactionValidatorFactory getPluginTransactionValidatorFactoryReturning( diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/AbstractTransactionReplacementTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/AbstractTransactionReplacementTest.java new file mode 100644 index 00000000000..f979fec3924 --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/AbstractTransactionReplacementTest.java @@ -0,0 +1,53 @@ +/* + * Copyright Hyperledger Besu Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.eth.transactions; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.datatypes.TransactionType; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.core.Transaction; + +import java.math.BigInteger; + +public class AbstractTransactionReplacementTest { + protected static PendingTransaction frontierTx(final long price) { + final PendingTransaction pendingTransaction = mock(PendingTransaction.class); + final Transaction transaction = + Transaction.builder() + .chainId(BigInteger.ZERO) + .type(TransactionType.FRONTIER) + .gasPrice(Wei.of(price)) + .build(); + when(pendingTransaction.getTransaction()).thenReturn(transaction); + when(pendingTransaction.getGasPrice()).thenReturn(Wei.of(price)); + return pendingTransaction; + } + + protected static PendingTransaction eip1559Tx( + final long maxPriorityFeePerGas, final long maxFeePerGas) { + final PendingTransaction pendingTransaction = mock(PendingTransaction.class); + final Transaction transaction = + Transaction.builder() + .chainId(BigInteger.ZERO) + .type(TransactionType.EIP1559) + .maxPriorityFeePerGas(Wei.of(maxPriorityFeePerGas)) + .maxFeePerGas(Wei.of(maxFeePerGas)) + .build(); + when(pendingTransaction.getTransaction()).thenReturn(transaction); + return pendingTransaction; + } +} diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByFeeMarketRuleTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByFeeMarketRuleTest.java new file mode 100644 index 00000000000..455f268e46f --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByFeeMarketRuleTest.java @@ -0,0 +1,100 @@ +/* + * Copyright Hyperledger Besu Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.eth.transactions; + +import static java.util.Arrays.asList; +import static java.util.Optional.empty; +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.util.number.Percentage; + +import java.util.Collection; +import java.util.Optional; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class TransactionReplacementByFeeMarketRuleTest extends AbstractTransactionReplacementTest { + + public static Collection data() { + return asList( + new Object[][] { + + // basefee absent + {frontierTx(5L), frontierTx(6L), empty(), 0, false}, + {frontierTx(5L), frontierTx(5L), empty(), 0, false}, + {frontierTx(5L), frontierTx(4L), empty(), 0, false}, + {frontierTx(100L), frontierTx(105L), empty(), 10, false}, + {frontierTx(100L), frontierTx(110L), empty(), 10, false}, + {frontierTx(100L), frontierTx(111L), empty(), 10, false}, + // basefee present + {frontierTx(5L), frontierTx(6L), Optional.of(Wei.of(3L)), 0, false}, + {frontierTx(5L), frontierTx(5L), Optional.of(Wei.of(3L)), 0, false}, + {frontierTx(5L), frontierTx(4L), Optional.of(Wei.of(3L)), 0, false}, + {frontierTx(100L), frontierTx(105L), Optional.of(Wei.of(3L)), 10, false}, + {frontierTx(100L), frontierTx(110L), Optional.of(Wei.of(3L)), 10, false}, + {frontierTx(100L), frontierTx(111L), Optional.of(Wei.of(3L)), 10, false}, + // eip1559 replacing frontier + {frontierTx(5L), eip1559Tx(3L, 6L), Optional.of(Wei.of(1L)), 0, false}, + {frontierTx(5L), eip1559Tx(3L, 5L), Optional.of(Wei.of(3L)), 0, true}, + {frontierTx(5L), eip1559Tx(3L, 6L), Optional.of(Wei.of(3L)), 0, true}, + // frontier replacing 1559 + {eip1559Tx(3L, 8L), frontierTx(7L), Optional.of(Wei.of(4L)), 0, true}, + {eip1559Tx(3L, 8L), frontierTx(7L), Optional.of(Wei.of(5L)), 0, false}, + {eip1559Tx(3L, 8L), frontierTx(8L), Optional.of(Wei.of(4L)), 0, true}, + // eip1559 replacing eip1559 + {eip1559Tx(3L, 6L), eip1559Tx(3L, 6L), Optional.of(Wei.of(3L)), 0, true}, + {eip1559Tx(3L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(3L)), 0, true}, + {eip1559Tx(3L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(4L)), 0, true}, + {eip1559Tx(3L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(5L)), 0, true}, + {eip1559Tx(3L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(6L)), 0, true}, + {eip1559Tx(3L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(7L)), 0, true}, + {eip1559Tx(3L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(8L)), 0, true}, + {eip1559Tx(10L, 200L), eip1559Tx(10L, 200L), Optional.of(Wei.of(90L)), 10, false}, + {eip1559Tx(10L, 200L), eip1559Tx(15L, 200L), Optional.of(Wei.of(90L)), 10, false}, + {eip1559Tx(10L, 200L), eip1559Tx(20L, 200L), Optional.of(Wei.of(90L)), 10, true}, + {eip1559Tx(10L, 200L), eip1559Tx(20L, 200L), Optional.of(Wei.of(200L)), 10, true}, + {eip1559Tx(10L, 200L), eip1559Tx(10L, 220L), Optional.of(Wei.of(220L)), 10, true}, + // pathological, priority fee > max fee + {eip1559Tx(8L, 6L), eip1559Tx(3L, 6L), Optional.of(Wei.of(2L)), 0, false}, + {eip1559Tx(8L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(3L)), 0, true}, + {eip1559Tx(8L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(4L)), 0, true}, + // pathological, eip1559 without basefee + {eip1559Tx(8L, 6L), eip1559Tx(3L, 7L), Optional.empty(), 0, false}, + {eip1559Tx(8L, 6L), eip1559Tx(3L, 7L), Optional.empty(), 0, false}, + // zero base fee market + {frontierTx(0L), frontierTx(0L), Optional.of(Wei.ZERO), 0, false}, + {eip1559Tx(0L, 0L), frontierTx(0L), Optional.of(Wei.ZERO), 0, true}, + {frontierTx(0L), eip1559Tx(0L, 0L), Optional.of(Wei.ZERO), 0, true}, + {eip1559Tx(0L, 0L), eip1559Tx(0L, 0L), Optional.of(Wei.ZERO), 0, true}, + }); + } + + @ParameterizedTest + @MethodSource("data") + public void shouldReplace( + final PendingTransaction oldTx, + final PendingTransaction newTx, + final Optional baseFee, + final int priceBump, + final boolean expected) { + + assertThat( + new TransactionReplacementByFeeMarketRule(Percentage.fromInt(priceBump)) + .shouldReplace(oldTx, newTx, baseFee)) + .isEqualTo(expected); + } +} diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByGasPriceRuleTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByGasPriceRuleTest.java new file mode 100644 index 00000000000..5b0b6ade211 --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementByGasPriceRuleTest.java @@ -0,0 +1,87 @@ +/* + * Copyright Hyperledger Besu Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.eth.transactions; + +import static java.util.Arrays.asList; +import static java.util.Optional.empty; +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.util.number.Percentage; + +import java.util.Collection; +import java.util.Optional; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class TransactionReplacementByGasPriceRuleTest extends AbstractTransactionReplacementTest { + + public static Collection data() { + return asList( + new Object[][] { + + // basefee absent + {frontierTx(5L), frontierTx(6L), empty(), 0, true}, + {frontierTx(5L), frontierTx(5L), empty(), 0, true}, + {frontierTx(5L), frontierTx(4L), empty(), 0, false}, + {frontierTx(100L), frontierTx(105L), empty(), 10, false}, + {frontierTx(100L), frontierTx(110L), empty(), 10, true}, + {frontierTx(100L), frontierTx(111L), empty(), 10, true}, + // basefee present + {frontierTx(5L), frontierTx(6L), Optional.of(Wei.of(3L)), 0, true}, + {frontierTx(5L), frontierTx(5L), Optional.of(Wei.of(3L)), 0, true}, + {frontierTx(5L), frontierTx(4L), Optional.of(Wei.of(3L)), 0, false}, + {frontierTx(100L), frontierTx(105L), Optional.of(Wei.of(3L)), 10, false}, + {frontierTx(100L), frontierTx(110L), Optional.of(Wei.of(3L)), 10, true}, + {frontierTx(100L), frontierTx(111L), Optional.of(Wei.of(3L)), 10, true}, + // eip1559 replacing frontier + {frontierTx(5L), eip1559Tx(3L, 6L), Optional.of(Wei.of(1L)), 0, false}, + {frontierTx(5L), eip1559Tx(3L, 5L), Optional.of(Wei.of(3L)), 0, false}, + {frontierTx(5L), eip1559Tx(3L, 6L), Optional.of(Wei.of(3L)), 0, false}, + // frontier replacing 1559 + {eip1559Tx(3L, 8L), frontierTx(7L), Optional.of(Wei.of(4L)), 0, false}, + {eip1559Tx(3L, 8L), frontierTx(8L), Optional.of(Wei.of(4L)), 0, false}, + // eip1559 replacing eip1559 + {eip1559Tx(3L, 6L), eip1559Tx(3L, 6L), Optional.of(Wei.of(3L)), 0, false}, + {eip1559Tx(3L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(3L)), 0, false}, + {eip1559Tx(3L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(4L)), 0, false}, + {eip1559Tx(10L, 200L), eip1559Tx(10L, 200L), Optional.of(Wei.of(90L)), 10, false}, + {eip1559Tx(10L, 200L), eip1559Tx(15L, 200L), Optional.of(Wei.of(90L)), 10, false}, + {eip1559Tx(10L, 200L), eip1559Tx(21L, 200L), Optional.of(Wei.of(90L)), 10, false}, + // pathological, priority fee > max fee + {eip1559Tx(8L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(3L)), 0, false}, + {eip1559Tx(8L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(4L)), 0, false}, + // pathological, eip1559 without basefee + {eip1559Tx(8L, 6L), eip1559Tx(3L, 7L), Optional.empty(), 0, false}, + {eip1559Tx(8L, 6L), eip1559Tx(3L, 7L), Optional.empty(), 0, false}, + }); + } + + @ParameterizedTest + @MethodSource("data") + public void shouldReplace( + final PendingTransaction oldTx, + final PendingTransaction newTx, + final Optional baseFee, + final int priceBump, + final boolean expected) { + + assertThat( + new TransactionReplacementByGasPriceRule(Percentage.fromInt(priceBump)) + .shouldReplace(oldTx, newTx, baseFee)) + .isEqualTo(expected); + } +} diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementRulesTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementRulesTest.java index f4488fe857d..00492e32266 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementRulesTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionReplacementRulesTest.java @@ -20,20 +20,17 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import org.hyperledger.besu.datatypes.TransactionType; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.util.number.Percentage; -import java.math.BigInteger; import java.util.Collection; import java.util.Optional; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -public class TransactionReplacementRulesTest { +public class TransactionReplacementRulesTest extends AbstractTransactionReplacementTest { public static Collection data() { return asList( @@ -41,39 +38,45 @@ public static Collection data() { // TransactionReplacementByGasPriceRule // basefee absent {frontierTx(5L), frontierTx(6L), empty(), 0, true}, - {frontierTx(5L), frontierTx(5L), empty(), 0, false}, + {frontierTx(5L), frontierTx(5L), empty(), 0, true}, {frontierTx(5L), frontierTx(4L), empty(), 0, false}, {frontierTx(100L), frontierTx(105L), empty(), 10, false}, - {frontierTx(100L), frontierTx(110L), empty(), 10, false}, + {frontierTx(100L), frontierTx(110L), empty(), 10, true}, {frontierTx(100L), frontierTx(111L), empty(), 10, true}, // basefee present {frontierTx(5L), frontierTx(6L), Optional.of(Wei.of(3L)), 0, true}, - {frontierTx(5L), frontierTx(5L), Optional.of(Wei.of(3L)), 0, false}, + {frontierTx(5L), frontierTx(5L), Optional.of(Wei.of(3L)), 0, true}, {frontierTx(5L), frontierTx(4L), Optional.of(Wei.of(3L)), 0, false}, {frontierTx(100L), frontierTx(105L), Optional.of(Wei.of(3L)), 10, false}, - {frontierTx(100L), frontierTx(110L), Optional.of(Wei.of(3L)), 10, false}, + {frontierTx(100L), frontierTx(110L), Optional.of(Wei.of(3L)), 10, true}, {frontierTx(100L), frontierTx(111L), Optional.of(Wei.of(3L)), 10, true}, // TransactionReplacementByFeeMarketRule // eip1559 replacing frontier {frontierTx(5L), eip1559Tx(3L, 6L), Optional.of(Wei.of(1L)), 0, false}, - {frontierTx(5L), eip1559Tx(3L, 5L), Optional.of(Wei.of(3L)), 0, false}, + {frontierTx(5L), eip1559Tx(3L, 5L), Optional.of(Wei.of(3L)), 0, true}, {frontierTx(5L), eip1559Tx(3L, 6L), Optional.of(Wei.of(3L)), 0, true}, // frontier replacing 1559 - {eip1559Tx(3L, 8L), frontierTx(7L), Optional.of(Wei.of(4L)), 0, false}, + {eip1559Tx(3L, 8L), frontierTx(6L), Optional.of(Wei.of(4L)), 0, false}, + {eip1559Tx(3L, 8L), frontierTx(7L), Optional.of(Wei.of(4L)), 0, true}, {eip1559Tx(3L, 8L), frontierTx(8L), Optional.of(Wei.of(4L)), 0, true}, // eip1559 replacing eip1559 - {eip1559Tx(3L, 6L), eip1559Tx(3L, 6L), Optional.of(Wei.of(3L)), 0, false}, - {eip1559Tx(3L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(3L)), 0, false}, + {eip1559Tx(3L, 6L), eip1559Tx(3L, 6L), Optional.of(Wei.of(3L)), 0, true}, + {eip1559Tx(3L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(3L)), 0, true}, {eip1559Tx(3L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(4L)), 0, true}, {eip1559Tx(10L, 200L), eip1559Tx(10L, 200L), Optional.of(Wei.of(90L)), 10, false}, {eip1559Tx(10L, 200L), eip1559Tx(15L, 200L), Optional.of(Wei.of(90L)), 10, false}, {eip1559Tx(10L, 200L), eip1559Tx(21L, 200L), Optional.of(Wei.of(90L)), 10, true}, // pathological, priority fee > max fee - {eip1559Tx(8L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(3L)), 0, false}, + {eip1559Tx(8L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(3L)), 0, true}, {eip1559Tx(8L, 6L), eip1559Tx(3L, 7L), Optional.of(Wei.of(4L)), 0, true}, // pathological, eip1559 without basefee {eip1559Tx(8L, 6L), eip1559Tx(3L, 7L), Optional.empty(), 0, false}, {eip1559Tx(8L, 6L), eip1559Tx(3L, 7L), Optional.empty(), 0, false}, + // zero base fee market + {frontierTx(0L), frontierTx(0L), Optional.of(Wei.ZERO), 0, true}, + {eip1559Tx(0L, 0L), frontierTx(0L), Optional.of(Wei.ZERO), 0, true}, + {frontierTx(0L), eip1559Tx(0L, 0L), Optional.of(Wei.ZERO), 0, true}, + {eip1559Tx(0L, 0L), eip1559Tx(0L, 0L), Optional.of(Wei.ZERO), 0, true}, }); } @@ -93,31 +96,4 @@ public void shouldReplace( .shouldReplace(oldTx, newTx, mockHeader)) .isEqualTo(expected); } - - private static PendingTransaction frontierTx(final long price) { - final PendingTransaction pendingTransaction = mock(PendingTransaction.class); - final Transaction transaction = - Transaction.builder() - .chainId(BigInteger.ZERO) - .type(TransactionType.FRONTIER) - .gasPrice(Wei.of(price)) - .build(); - when(pendingTransaction.getTransaction()).thenReturn(transaction); - when(pendingTransaction.getGasPrice()).thenReturn(Wei.of(price)); - return pendingTransaction; - } - - private static PendingTransaction eip1559Tx( - final long maxPriorityFeePerGas, final long maxFeePerGas) { - final PendingTransaction pendingTransaction = mock(PendingTransaction.class); - final Transaction transaction = - Transaction.builder() - .chainId(BigInteger.ZERO) - .type(TransactionType.EIP1559) - .maxPriorityFeePerGas(Wei.of(maxPriorityFeePerGas)) - .maxFeePerGas(Wei.of(maxFeePerGas)) - .build(); - when(pendingTransaction.getTransaction()).thenReturn(transaction); - return pendingTransaction; - } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/LegacyTransactionPoolBaseFeeTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/LegacyTransactionPoolBaseFeeTest.java index 96ea292e983..1f1d21b0a52 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/LegacyTransactionPoolBaseFeeTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/LegacyTransactionPoolBaseFeeTest.java @@ -21,13 +21,11 @@ import org.hyperledger.besu.ethereum.core.ExecutionContextTestFixture; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.core.TransactionTestFixture; -import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; import org.hyperledger.besu.testutil.TestClock; -import org.hyperledger.besu.util.number.Fraction; import java.time.ZoneId; import java.util.Optional; @@ -42,10 +40,7 @@ protected PendingTransactions createPendingTransactions( transactionReplacementTester) { return new BaseFeePendingTransactionsSorter( - ImmutableTransactionPoolConfiguration.builder() - .txPoolMaxSize(MAX_TRANSACTIONS) - .txPoolLimitByAccountPercentage(Fraction.fromFloat(1.0f)) - .build(), + poolConfig, TestClock.system(ZoneId.systemDefault()), metricsSystem, protocolContext.getBlockchain()::getChainHeadHeader); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/LegacyTransactionPoolGasPriceTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/LegacyTransactionPoolGasPriceTest.java index 5d20e04de0e..e4abb7a2536 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/LegacyTransactionPoolGasPriceTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/LegacyTransactionPoolGasPriceTest.java @@ -21,13 +21,11 @@ import org.hyperledger.besu.ethereum.core.ExecutionContextTestFixture; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.core.TransactionTestFixture; -import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; import org.hyperledger.besu.testutil.TestClock; -import org.hyperledger.besu.util.number.Fraction; import java.time.ZoneId; import java.util.function.BiFunction; @@ -41,10 +39,7 @@ protected PendingTransactions createPendingTransactions( transactionReplacementTester) { return new GasPricePendingTransactionsSorter( - ImmutableTransactionPoolConfiguration.builder() - .txPoolMaxSize(MAX_TRANSACTIONS) - .txPoolLimitByAccountPercentage(Fraction.fromFloat(1.0f)) - .build(), + poolConfig, TestClock.system(ZoneId.systemDefault()), metricsSystem, protocolContext.getBlockchain()::getChainHeadHeader); diff --git a/util/src/main/java/org/hyperledger/besu/util/number/Percentage.java b/util/src/main/java/org/hyperledger/besu/util/number/Percentage.java index 17c58a7f375..ccbbc2c1295 100644 --- a/util/src/main/java/org/hyperledger/besu/util/number/Percentage.java +++ b/util/src/main/java/org/hyperledger/besu/util/number/Percentage.java @@ -21,6 +21,8 @@ /** The Percentage utility. */ public class Percentage { + /** Represent 0% */ + public static final Percentage ZERO = new Percentage(0); private final int value;