From ad4451b0b83c78c2302a5f4302b74a7574103232 Mon Sep 17 00:00:00 2001 From: Angel Soto Date: Fri, 15 Dec 2023 19:42:21 +0100 Subject: [PATCH] Minor PR updates, update config path for this feature Updating from weighted average to weighted percentile method and naming updates Minor improvements related with PR comments Adding GasWeighted calculator and extracted legacy calculation to be able to choose which one could be used. --- .../src/main/java/co/rsk/RskContext.java | 4 +- .../co/rsk/config/RskSystemProperties.java | 15 ++ .../ethereum/listener/GasPriceCalculator.java | 51 +++++ .../ethereum/listener/GasPriceTracker.java | 82 +++---- .../PercentileGasPriceCalculator.java | 78 +++++++ .../listener/WeightedPercentileCalc.java | 50 +++++ .../WeightedPercentileGasPriceCalculator.java | 141 ++++++++++++ rskj-core/src/main/resources/expected.conf | 1 + rskj-core/src/main/resources/reference.conf | 4 +- .../test/java/co/rsk/NodeRunnerSmokeTest.java | 2 +- .../listener/GasPriceTrackerTest.java | 16 +- .../listener/WeightedPercentileCalcTest.java | 76 +++++++ ...ghtedPercentileGasPriceCalculatorTest.java | 204 ++++++++++++++++++ 13 files changed, 679 insertions(+), 45 deletions(-) create mode 100644 rskj-core/src/main/java/org/ethereum/listener/GasPriceCalculator.java create mode 100644 rskj-core/src/main/java/org/ethereum/listener/PercentileGasPriceCalculator.java create mode 100644 rskj-core/src/main/java/org/ethereum/listener/WeightedPercentileCalc.java create mode 100644 rskj-core/src/main/java/org/ethereum/listener/WeightedPercentileGasPriceCalculator.java create mode 100644 rskj-core/src/test/java/org/ethereum/listener/WeightedPercentileCalcTest.java create mode 100644 rskj-core/src/test/java/org/ethereum/listener/WeightedPercentileGasPriceCalculatorTest.java diff --git a/rskj-core/src/main/java/co/rsk/RskContext.java b/rskj-core/src/main/java/co/rsk/RskContext.java index f9ced02420c..8a80eec9deb 100644 --- a/rskj-core/src/main/java/co/rsk/RskContext.java +++ b/rskj-core/src/main/java/co/rsk/RskContext.java @@ -105,6 +105,7 @@ import org.ethereum.facade.Ethereum; import org.ethereum.facade.EthereumImpl; import org.ethereum.listener.CompositeEthereumListener; +import org.ethereum.listener.GasPriceCalculator; import org.ethereum.listener.GasPriceTracker; import org.ethereum.net.EthereumChannelInitializerFactory; import org.ethereum.net.NodeManager; @@ -558,7 +559,8 @@ public GasPriceTracker getGasPriceTracker() { double gasPriceMultiplier = getRskSystemProperties().gasPriceMultiplier(); if (this.gasPriceTracker == null) { - this.gasPriceTracker = GasPriceTracker.create(getBlockStore(), gasPriceMultiplier); + GasPriceCalculator.GasCalculatorType calculatorType = getRskSystemProperties().getGasCalculatorType(); + this.gasPriceTracker = GasPriceTracker.create(getBlockStore(), gasPriceMultiplier, calculatorType); } return this.gasPriceTracker; } diff --git a/rskj-core/src/main/java/co/rsk/config/RskSystemProperties.java b/rskj-core/src/main/java/co/rsk/config/RskSystemProperties.java index 57ee131baa2..c1ee1116750 100644 --- a/rskj-core/src/main/java/co/rsk/config/RskSystemProperties.java +++ b/rskj-core/src/main/java/co/rsk/config/RskSystemProperties.java @@ -30,6 +30,7 @@ import org.ethereum.core.Account; import org.ethereum.crypto.ECKey; import org.ethereum.crypto.HashUtil; +import org.ethereum.listener.GasPriceCalculator; import javax.annotation.Nullable; import java.nio.charset.StandardCharsets; @@ -58,6 +59,8 @@ public class RskSystemProperties extends SystemProperties { private static final String RPC_MODULES_PATH = "rpc.modules"; private static final String RPC_ETH_GET_LOGS_MAX_BLOCKS_TO_QUERY = "rpc.logs.maxBlocksToQuery"; private static final String RPC_ETH_GET_LOGS_MAX_LOGS_TO_RETURN = "rpc.logs.maxLogsToReturn"; + public static final String TX_GAS_PRICE_CALCULATOR_TYPE = "transaction.gasPriceCalculatorType"; + private static final String RPC_GAS_PRICE_MULTIPLIER_CONFIG = "rpc.gasPriceMultiplier"; private static final String DISCOVERY_BUCKET_SIZE = "peer.discovery.bucketSize"; @@ -506,6 +509,18 @@ public double getTopBest() { return value; } + public GasPriceCalculator.GasCalculatorType getGasCalculatorType() { + String value = configFromFiles.getString(TX_GAS_PRICE_CALCULATOR_TYPE); + if (value == null || value.isEmpty()) { + return GasPriceCalculator.GasCalculatorType.PLAIN_PERCENTILE; + } + GasPriceCalculator.GasCalculatorType gasCalculatorType = GasPriceCalculator.GasCalculatorType.fromString(value); + if(gasCalculatorType == null) { + throw new RskConfigurationException("Invalid gasPriceCalculatorType: " + value); + } + return gasCalculatorType; + } + private void fetchMethodTimeout(Config configElement, Map methodTimeoutMap) { configElement.getObject("methods.timeout") .unwrapped() diff --git a/rskj-core/src/main/java/org/ethereum/listener/GasPriceCalculator.java b/rskj-core/src/main/java/org/ethereum/listener/GasPriceCalculator.java new file mode 100644 index 00000000000..a8be9510a34 --- /dev/null +++ b/rskj-core/src/main/java/org/ethereum/listener/GasPriceCalculator.java @@ -0,0 +1,51 @@ +/* + * 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 org.ethereum.listener; + +import co.rsk.core.Coin; +import org.ethereum.core.Block; +import org.ethereum.core.TransactionReceipt; + +import java.util.List; +import java.util.Optional; + +public interface GasPriceCalculator { + public enum GasCalculatorType { + PLAIN_PERCENTILE, + WEIGHTED_PERCENTILE; + + public static GasCalculatorType fromString(String type) { + if (type == null) { + return null; + } + switch (type.toLowerCase()) { + case "weighted_percentile": + return WEIGHTED_PERCENTILE; + case "plain_percentile": + return PLAIN_PERCENTILE; + default: + return null; + } + } + } + + Optional getGasPrice(); + void onBlock(Block block, List receipts); + + GasCalculatorType getType(); +} diff --git a/rskj-core/src/main/java/org/ethereum/listener/GasPriceTracker.java b/rskj-core/src/main/java/org/ethereum/listener/GasPriceTracker.java index d9a29d489c4..1d19ba277fb 100644 --- a/rskj-core/src/main/java/org/ethereum/listener/GasPriceTracker.java +++ b/rskj-core/src/main/java/org/ethereum/listener/GasPriceTracker.java @@ -23,7 +23,6 @@ import co.rsk.crypto.Keccak256; import co.rsk.remasc.RemascTransaction; import org.ethereum.core.Block; -import org.ethereum.core.Transaction; import org.ethereum.core.TransactionReceipt; import org.ethereum.db.BlockStore; import org.slf4j.Logger; @@ -35,10 +34,10 @@ /** * Calculates a 'reasonable' Gas price based on statistics of the latest transaction's Gas prices - * + *

* Normally the price returned should be sufficient to execute a transaction since ~25% of the latest * transactions were executed at this or lower price. - * + *

* Created by Anton Nashatyrev on 22.09.2015. */ public class GasPriceTracker extends EthereumListenerAdapter { @@ -53,8 +52,6 @@ public class GasPriceTracker extends EthereumListenerAdapter { private static final double DEFAULT_GAS_PRICE_MULTIPLIER = 1.1; - private final Coin[] txWindow = new Coin[TX_WINDOW_SIZE]; - private final Double[] blockWindow = new Double[BLOCK_WINDOW_SIZE]; private final AtomicReference bestBlockPriceRef = new AtomicReference<>(); @@ -62,27 +59,47 @@ public class GasPriceTracker extends EthereumListenerAdapter { private final double gasPriceMultiplier; private Coin defaultPrice = Coin.valueOf(20_000_000_000L); - private int txIdx = TX_WINDOW_SIZE - 1; - private int blockIdx = 0; - private Coin lastVal; + private final GasPriceCalculator gasPriceCalculator; - private GasPriceTracker(BlockStore blockStore, Double configMultiplier) { + private GasPriceTracker(BlockStore blockStore, GasPriceCalculator gasPriceCalculator, Double configMultiplier) { this.blockStore = blockStore; + this.gasPriceCalculator = gasPriceCalculator; this.gasPriceMultiplier = configMultiplier; } - public static GasPriceTracker create(BlockStore blockStore) { - return create(blockStore, DEFAULT_GAS_PRICE_MULTIPLIER); + public static GasPriceTracker create(BlockStore blockStore, GasPriceCalculator.GasCalculatorType gasCalculatorType) { + return create(blockStore, DEFAULT_GAS_PRICE_MULTIPLIER, gasCalculatorType); } - public static GasPriceTracker create(BlockStore blockStore, Double configMultiplier) { - GasPriceTracker gasPriceTracker = new GasPriceTracker(blockStore, configMultiplier); + public static GasPriceTracker create(BlockStore blockStore, Double configMultiplier, GasPriceCalculator.GasCalculatorType gasCalculatorType) { + GasPriceCalculator gasCal; + switch (gasCalculatorType) { + case WEIGHTED_PERCENTILE: + gasCal = new WeightedPercentileGasPriceCalculator(); + break; + case PLAIN_PERCENTILE: + gasCal = new PercentileGasPriceCalculator(); + break; + default: + throw new IllegalArgumentException("Unknown gas calculator type: " + gasCalculatorType); + } + GasPriceTracker gasPriceTracker = new GasPriceTracker(blockStore, gasCal, configMultiplier); gasPriceTracker.initializeWindowsFromDB(); + return gasPriceTracker; } + /** + * @deprecated Use {@link #create(BlockStore, GasPriceCalculator.GasCalculatorType)} instead. + */ + @Deprecated + public static GasPriceTracker create(BlockStore blockStore) { + //Will be using the legacy gas calculator as default option + return GasPriceTracker.create(blockStore, GasPriceCalculator.GasCalculatorType.PLAIN_PERCENTILE); + } + @Override public void onBestBlock(Block block, List receipts) { bestBlockPriceRef.set(block.getMinimumGasPrice()); @@ -96,38 +113,25 @@ public synchronized void onBlock(Block block, List receipts) trackBlockCompleteness(block); - for (Transaction tx : block.getTransactionsList()) { - onTransaction(tx); - } - + gasPriceCalculator.onBlock(block, receipts); logger.trace("End onBlock"); } - private void onTransaction(Transaction tx) { - if (tx instanceof RemascTransaction) { - return; - } - - trackGasPrice(tx); - } - public synchronized Coin getGasPrice() { - if (txWindow[0] == null) { // for some reason, not filled yet (i.e. not enough blocks on DB) + Optional gasPriceResult = gasPriceCalculator.getGasPrice(); + if(!gasPriceResult.isPresent()) { return defaultPrice; } - if (lastVal == null) { - Coin[] values = Arrays.copyOf(txWindow, TX_WINDOW_SIZE); - Arrays.sort(values); - lastVal = values[values.length / 4]; // 25% percentile - } + logger.debug("Gas provided by GasWindowCalc: {}", gasPriceResult.get()); Coin bestBlockPrice = bestBlockPriceRef.get(); if (bestBlockPrice == null) { - return lastVal; + logger.debug("Best block price not available, defaulting to {}", gasPriceResult.get()); + return gasPriceResult.get(); } - return Coin.max(lastVal, new Coin(new BigDecimal(bestBlockPrice.asBigInteger()) + return Coin.max(gasPriceResult.get(), new Coin(new BigDecimal(bestBlockPrice.asBigInteger()) .multiply(BigDecimal.valueOf(gasPriceMultiplier)).toBigInteger())); } @@ -180,14 +184,6 @@ private List getRequiredBlocksToFillWindowsFromDB() { return blocks; } - private void trackGasPrice(Transaction tx) { - if (txIdx == -1) { - txIdx = TX_WINDOW_SIZE - 1; - lastVal = null; // recalculate only 'sometimes' - } - txWindow[txIdx--] = tx.getGasPrice(); - } - private void trackBlockCompleteness(Block block) { double gasUsed = block.getGasUsed(); double gasLimit = block.getGasLimitAsInteger().doubleValue(); @@ -199,4 +195,8 @@ private void trackBlockCompleteness(Block block) { blockWindow[blockIdx++] = completeness; } + public GasPriceCalculator.GasCalculatorType getGasCalculatorType() { + return gasPriceCalculator.getType(); + } + } diff --git a/rskj-core/src/main/java/org/ethereum/listener/PercentileGasPriceCalculator.java b/rskj-core/src/main/java/org/ethereum/listener/PercentileGasPriceCalculator.java new file mode 100644 index 00000000000..5a4ccaa393e --- /dev/null +++ b/rskj-core/src/main/java/org/ethereum/listener/PercentileGasPriceCalculator.java @@ -0,0 +1,78 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * (derived from ethereumJ library, Copyright (c) 2016 ) + * + * 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 org.ethereum.listener; + +import co.rsk.core.Coin; +import co.rsk.remasc.RemascTransaction; +import org.ethereum.core.Block; +import org.ethereum.core.Transaction; +import org.ethereum.core.TransactionReceipt; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +public class PercentileGasPriceCalculator implements GasPriceCalculator { + private static final int TX_WINDOW_SIZE = 512; + + private final Coin[] txWindow = new Coin[TX_WINDOW_SIZE]; + private int txIdx = TX_WINDOW_SIZE - 1; + private Coin lastVal; + + @Override + public synchronized Optional getGasPrice() { + if (txWindow[0] == null) { // for some reason, not filled yet (i.e. not enough blocks on DB) + return Optional.empty(); + } else { + if (lastVal == null) { + Coin[] values = Arrays.copyOf(txWindow, TX_WINDOW_SIZE); + Arrays.sort(values); + lastVal = values[values.length / 4]; // 25% percentile + } + return Optional.of(lastVal); + } + } + + @Override + public synchronized void onBlock(Block block, List receipts) { + onBlock(block.getTransactionsList()); + } + + @Override + public GasCalculatorType getType() { + return GasCalculatorType.PLAIN_PERCENTILE; + } + + private void onBlock(List transactionList) { + for (Transaction tx : transactionList) { + if (!(tx instanceof RemascTransaction)) { + trackGasPrice(tx); + } + } + } + + private void trackGasPrice(Transaction tx) { + if (txIdx == -1) { + txIdx = TX_WINDOW_SIZE - 1; + lastVal = null; // recalculate only 'sometimes' + } + txWindow[txIdx--] = tx.getGasPrice(); + } + +} diff --git a/rskj-core/src/main/java/org/ethereum/listener/WeightedPercentileCalc.java b/rskj-core/src/main/java/org/ethereum/listener/WeightedPercentileCalc.java new file mode 100644 index 00000000000..046fc62f69a --- /dev/null +++ b/rskj-core/src/main/java/org/ethereum/listener/WeightedPercentileCalc.java @@ -0,0 +1,50 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * (derived from ethereumJ library, Copyright (c) 2016 ) + * + * 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 org.ethereum.listener; + +import co.rsk.core.Coin; + +import java.util.Collections; +import java.util.List; + +class WeightedPercentileCalc { + + Coin calculateWeightedPercentile(float percentile, List gasEntries) { + if (gasEntries == null || gasEntries.isEmpty()) { + return null; + } + + Collections.sort(gasEntries); + + double totalWeight = gasEntries.stream().mapToLong(WeightedPercentileGasPriceCalculator.GasEntry::getGasUsed).sum(); + + double targetWeight = percentile / 100 * totalWeight; + + + double cumulativeWeight = 0; + for (WeightedPercentileGasPriceCalculator.GasEntry pair : gasEntries) { + cumulativeWeight += pair.getGasUsed(); + if (cumulativeWeight >= targetWeight) { + return pair.getGasPrice(); + } + } + + return null; + } +} diff --git a/rskj-core/src/main/java/org/ethereum/listener/WeightedPercentileGasPriceCalculator.java b/rskj-core/src/main/java/org/ethereum/listener/WeightedPercentileGasPriceCalculator.java new file mode 100644 index 00000000000..673573c491c --- /dev/null +++ b/rskj-core/src/main/java/org/ethereum/listener/WeightedPercentileGasPriceCalculator.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 org.ethereum.listener; + +import co.rsk.core.Coin; +import co.rsk.remasc.RemascTransaction; +import org.ethereum.core.Block; +import org.ethereum.core.Transaction; +import org.ethereum.core.TransactionReceipt; + +import java.util.*; + +public class WeightedPercentileGasPriceCalculator implements GasPriceCalculator { + private static final int WINDOW_SIZE = 512; + public static final int REFERENCE_PERCENTILE = 25; + + private final ArrayDeque gasWindow; + private final WeightedPercentileCalc auxCalculator; + + private int txCount = 0; + private Coin cachedGasPrice = null; + + public WeightedPercentileGasPriceCalculator() { + this(new WeightedPercentileCalc()); + } + + public WeightedPercentileGasPriceCalculator(WeightedPercentileCalc weightedPercentileCalc) { + auxCalculator = weightedPercentileCalc; + gasWindow = new ArrayDeque<>(WINDOW_SIZE); + } + + @Override + public synchronized Optional getGasPrice() { + if (cachedGasPrice == null) { + cachedGasPrice = calculateGasPrice(); + } + return cachedGasPrice == null ? Optional.empty() : Optional.of(cachedGasPrice); + } + + @Override + public synchronized void onBlock(Block block, List receipts) { + for (TransactionReceipt receipt : receipts) { + if (!(receipt.getTransaction() instanceof RemascTransaction)) { + addTx(receipt.getTransaction(), new Coin(receipt.getGasUsed()).asBigInteger().longValue()); + } + } + } + + @Override + public GasCalculatorType getType() { + return GasCalculatorType.WEIGHTED_PERCENTILE; + } + + private void addTx(Transaction tx, long gasUsed) { + if (gasUsed == 0) { + return; + } + + txCount++; + + Coin gasPrice = tx.getGasPrice(); + + if (gasWindow.size() == WINDOW_SIZE) { + gasWindow.removeFirst(); + + } + gasWindow.add(new GasEntry(gasPrice, gasUsed)); + + if (txCount > WINDOW_SIZE) { + txCount = 0; // Reset the count + cachedGasPrice = null; // Invalidate the cached value to force recalculation when queried. + } + } + + private Coin calculateGasPrice() { + return auxCalculator.calculateWeightedPercentile(REFERENCE_PERCENTILE, new ArrayList<>(gasWindow)); + } + + static class GasEntry implements Comparable { + protected Coin gasPrice; + protected long gasUsed; + + GasEntry(Coin gasPrice, long gasUsed) { + this.gasPrice = gasPrice; + this.gasUsed = gasUsed; + } + + + public Coin getGasPrice() { + return gasPrice; + } + + public long getGasUsed() { + return gasUsed; + } + + @Override + public int compareTo + (GasEntry o) { + return this.gasPrice.compareTo(o.gasPrice); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof GasEntry)) { + return false; + } + GasEntry gasEntry = (GasEntry) o; + return gasUsed == gasEntry.gasUsed && + Objects.equals(gasPrice, gasEntry.gasPrice); + } + + @Override + public int hashCode() { + return Objects.hash(gasPrice, gasUsed); + } + + @Override + public String toString() { + return "(" + gasPrice + ", " + gasUsed + ")"; + } + } +} diff --git a/rskj-core/src/main/resources/expected.conf b/rskj-core/src/main/resources/expected.conf index 8b6461c0167..b48f4432a46 100644 --- a/rskj-core/src/main/resources/expected.conf +++ b/rskj-core/src/main/resources/expected.conf @@ -204,6 +204,7 @@ transaction = { threshold = timeout = } + gasPriceCalculatorType = gasPriceBump = accountSlots = accountTxRateLimit = { diff --git a/rskj-core/src/main/resources/reference.conf b/rskj-core/src/main/resources/reference.conf index 5a2c3c7153a..f82fa66a6d9 100644 --- a/rskj-core/src/main/resources/reference.conf +++ b/rskj-core/src/main/resources/reference.conf @@ -167,7 +167,6 @@ peer { miner { # The default gas price minGasPrice = 0 - server { enabled = false isFixedClock = false @@ -234,6 +233,9 @@ transaction.outdated.threshold = 10 # (suggested value: 10 blocks * 10 seconds by block = 100 seconds) transaction.outdated.timeout = 650 +# choose the type of gas price calculator being PLAIN_PERCENTILE or WEIGHTED_PERCENTILE. The gas used by tx is taken into account only in WEIGHTED_PERCENTILE +transaction.gasPriceCalculatorType = PLAIN_PERCENTILE + # the percentage increase of gasPrice defined to accept a new transaction # with same nonce and sender while the previous one is not yet processed transaction.gasPriceBump = 40 diff --git a/rskj-core/src/test/java/co/rsk/NodeRunnerSmokeTest.java b/rskj-core/src/test/java/co/rsk/NodeRunnerSmokeTest.java index 0815c86ff7b..d435196a0d6 100644 --- a/rskj-core/src/test/java/co/rsk/NodeRunnerSmokeTest.java +++ b/rskj-core/src/test/java/co/rsk/NodeRunnerSmokeTest.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ - + package co.rsk; import org.ethereum.util.RskTestContext; diff --git a/rskj-core/src/test/java/org/ethereum/listener/GasPriceTrackerTest.java b/rskj-core/src/test/java/org/ethereum/listener/GasPriceTrackerTest.java index 07382c02356..a76f9f83815 100644 --- a/rskj-core/src/test/java/org/ethereum/listener/GasPriceTrackerTest.java +++ b/rskj-core/src/test/java/org/ethereum/listener/GasPriceTrackerTest.java @@ -152,7 +152,7 @@ void getGasPrice_PriceWindowFilled_BestBlockReceivedWithGreaterPrice_ReturnsBest @Test void getGasPrice_PriceWindowFilled_BestBlockReceivedWithGreaterPrice_GasPriceMultiplierOverWritten_ReturnsBestBlockAdjustedPriceWithNewBuffer() { - GasPriceTracker gasPriceTracker = GasPriceTracker.create(blockStore, 1.05); + GasPriceTracker gasPriceTracker = GasPriceTracker.create(blockStore, 1.05, GasPriceCalculator.GasCalculatorType.PLAIN_PERCENTILE); Block bestBlock = makeBlock(Coin.valueOf(50_000_000_000L), 0, i -> null); Block block = makeBlock(Coin.valueOf(30_000_000_000L), TOTAL_SLOTS, i -> makeTx(Coin.valueOf(40_000_000_000L))); @@ -209,6 +209,20 @@ void isFeeMarketWorking_trueWhenAboveAverage() { assertTrue(gasPriceTracker.isFeeMarketWorking()); } + @Test + void gasTrackerIsCreatedWithTheCorrectType() { + GasPriceTracker gasPriceTracker = GasPriceTracker.create(blockStore); + assertEquals(GasPriceCalculator.GasCalculatorType.PLAIN_PERCENTILE, gasPriceTracker.getGasCalculatorType(), "Plain pecentile is the default one"); + + assertEquals(GasPriceCalculator.GasCalculatorType.PLAIN_PERCENTILE, + GasPriceTracker.create(blockStore, GasPriceCalculator.GasCalculatorType.PLAIN_PERCENTILE).getGasCalculatorType(), + "Plain percentile type is expected when passed as parameter"); + + assertEquals(GasPriceCalculator.GasCalculatorType.WEIGHTED_PERCENTILE, + GasPriceTracker.create(blockStore, GasPriceCalculator.GasCalculatorType.WEIGHTED_PERCENTILE).getGasCalculatorType(), + "Weighted percentile type is expected when passed as parameter"); + } + private static Block makeBlock(Coin mgp, int txCount, Function txMaker) { Block block = mock(Block.class); diff --git a/rskj-core/src/test/java/org/ethereum/listener/WeightedPercentileCalcTest.java b/rskj-core/src/test/java/org/ethereum/listener/WeightedPercentileCalcTest.java new file mode 100644 index 00000000000..cf048f8ffbb --- /dev/null +++ b/rskj-core/src/test/java/org/ethereum/listener/WeightedPercentileCalcTest.java @@ -0,0 +1,76 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * (derived from ethereumJ library, Copyright (c) 2016 ) + * + * 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 org.ethereum.listener; + +import co.rsk.core.Coin; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WeightedPercentileCalcTest { + + @Test + void testCalculateWeightedPercentile() { + WeightedPercentileCalc weightedPercentileCalc = new WeightedPercentileCalc(); + + // Sample gas entries with smaller numbers + WeightedPercentileGasPriceCalculator.GasEntry entry1 = new WeightedPercentileGasPriceCalculator.GasEntry(new Coin(BigInteger.valueOf(100)), 1); + WeightedPercentileGasPriceCalculator.GasEntry entry2 = new WeightedPercentileGasPriceCalculator.GasEntry(new Coin(BigInteger.valueOf(200)), 3); + WeightedPercentileGasPriceCalculator.GasEntry entry3 = new WeightedPercentileGasPriceCalculator.GasEntry(new Coin(BigInteger.valueOf(300)), 1); + WeightedPercentileGasPriceCalculator.GasEntry entry4 = new WeightedPercentileGasPriceCalculator.GasEntry(new Coin(BigInteger.valueOf(500)), 10); + WeightedPercentileGasPriceCalculator.GasEntry entry5 = new WeightedPercentileGasPriceCalculator.GasEntry(new Coin(BigInteger.valueOf(400)), 1); + WeightedPercentileGasPriceCalculator.GasEntry entry6 = new WeightedPercentileGasPriceCalculator.GasEntry(new Coin(BigInteger.valueOf(700)), 2); + WeightedPercentileGasPriceCalculator.GasEntry entry7 = new WeightedPercentileGasPriceCalculator.GasEntry(new Coin(BigInteger.valueOf(600)), 4); + WeightedPercentileGasPriceCalculator.GasEntry entry8 = new WeightedPercentileGasPriceCalculator.GasEntry(new Coin(BigInteger.valueOf(800)), 1); + + + List gasEntries = Arrays.asList(entry1, entry2, entry3, entry4, entry5, entry6, entry7,entry8); + + + Coin result0 = weightedPercentileCalc.calculateWeightedPercentile(0, gasEntries); + assertEquals(new Coin(BigInteger.valueOf(100)), result0, "0th percentile should be 100"); + + Coin result10 = weightedPercentileCalc.calculateWeightedPercentile(1, gasEntries); + assertEquals(new Coin(BigInteger.valueOf(100)), result10, "1th percentile should be 100"); + + Coin result20 = weightedPercentileCalc.calculateWeightedPercentile(20.2f, gasEntries); + assertEquals(new Coin(BigInteger.valueOf(300)), result20, "20th percentile should be 300"); + + Coin result40 = weightedPercentileCalc.calculateWeightedPercentile(40, gasEntries); + assertEquals(new Coin(BigInteger.valueOf(500)), result40, "40th percentile should be 500"); + + Coin result50 = weightedPercentileCalc.calculateWeightedPercentile(50, gasEntries); + assertEquals(new Coin(BigInteger.valueOf(500)), result50, "50th percentile should be 500"); + + Coin result75 = weightedPercentileCalc.calculateWeightedPercentile(75, gasEntries); + assertEquals(new Coin(BigInteger.valueOf(600)), result75, "75th percentile should be 600"); + + Coin result90 = weightedPercentileCalc.calculateWeightedPercentile(90, gasEntries); + assertEquals(new Coin(BigInteger.valueOf(700)), result90, "90th percentile should be 600"); + + Coin result100 = weightedPercentileCalc.calculateWeightedPercentile(100, gasEntries); + assertEquals(new Coin(BigInteger.valueOf(800)), result100, "100th percentile should be 800"); + + + } +} \ No newline at end of file diff --git a/rskj-core/src/test/java/org/ethereum/listener/WeightedPercentileGasPriceCalculatorTest.java b/rskj-core/src/test/java/org/ethereum/listener/WeightedPercentileGasPriceCalculatorTest.java new file mode 100644 index 00000000000..597f5191202 --- /dev/null +++ b/rskj-core/src/test/java/org/ethereum/listener/WeightedPercentileGasPriceCalculatorTest.java @@ -0,0 +1,204 @@ +/* + * This file is part of RskJ + * Copyright (C) 2024 RSK Labs Ltd. + * (derived from ethereumJ library, Copyright (c) 2016 ) + * + * 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 org.ethereum.listener; + +import co.rsk.core.Coin; +import org.ethereum.core.Block; +import org.ethereum.core.Transaction; +import org.ethereum.core.TransactionReceipt; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.math.BigInteger; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class WeightedPercentileGasPriceCalculatorTest { + + private static final int WINDOW_SIZE = 512; + private WeightedPercentileGasPriceCalculator weightedPercentileGasPriceCalculator; + + + @BeforeEach + void setup() { + weightedPercentileGasPriceCalculator = new WeightedPercentileGasPriceCalculator(); + } + + @Test + void testCalculateWeightedPercentileWithNoTransactions() { + // Test when no transactions are added + assertNotNull(weightedPercentileGasPriceCalculator); + assertFalse(weightedPercentileGasPriceCalculator.getGasPrice().isPresent(), "Gas price should not be present when no transactions are added"); + } + + @Test + void testCalculateGasPriceWithZeroTotalGasUsed() { + // Test when the total gas used is zero + Block mockBlock = Mockito.mock(Block.class); + + TransactionReceipt mockReceipt = Mockito.mock(TransactionReceipt.class); + Transaction mockTransaction = Mockito.mock(Transaction.class); + when(mockTransaction.getGasPrice()).thenReturn(new Coin(BigInteger.valueOf(100))); + when(mockReceipt.getTransaction()).thenReturn(mockTransaction); + when(mockReceipt.getGasUsed()).thenReturn(BigInteger.ZERO.toByteArray()); + + weightedPercentileGasPriceCalculator.onBlock(mockBlock, Collections.singletonList(mockReceipt)); + + Optional gasPrice = weightedPercentileGasPriceCalculator.getGasPrice(); + assertFalse(gasPrice.isPresent(), "Gas price should not be present when total gas used is zero"); + } + + @Test + void testCalculateGasPriceWithSingleTransaction() { + // Test when a single transaction is added + Block mockBlock = Mockito.mock(Block.class); + + TransactionReceipt mockReceipt = Mockito.mock(TransactionReceipt.class); + Transaction mockTransaction = Mockito.mock(Transaction.class); + when(mockTransaction.getGasPrice()).thenReturn(new Coin(BigInteger.valueOf(100))); + when(mockReceipt.getTransaction()).thenReturn(mockTransaction); + when(mockReceipt.getGasUsed()).thenReturn(BigInteger.valueOf(500).toByteArray()); + + weightedPercentileGasPriceCalculator.onBlock(mockBlock, Collections.singletonList(mockReceipt)); + + Optional gasPrice = weightedPercentileGasPriceCalculator.getGasPrice(); + assertTrue(gasPrice.isPresent(), "Gas price should be present when a transaction is added"); + assertEquals(new Coin(BigInteger.valueOf(100)), gasPrice.get(), "Gas price should be the same as the single transaction's gas price"); + } + + @Test + void testCalculateGasPriceWithMultipleTransactionsSameGasUsage() { + // Test when multiple transactions are added + Block mockBlock = Mockito.mock(Block.class); + + TransactionReceipt mockReceipt1 = Mockito.mock(TransactionReceipt.class); + Transaction mockTransaction1 = Mockito.mock(Transaction.class); + when(mockTransaction1.getGasPrice()).thenReturn(new Coin(BigInteger.valueOf(100))); + when(mockReceipt1.getTransaction()).thenReturn(mockTransaction1); + when(mockReceipt1.getGasUsed()).thenReturn(BigInteger.valueOf(100).toByteArray()); + + TransactionReceipt mockReceipt2 = Mockito.mock(TransactionReceipt.class); + Transaction mockTransaction2 = Mockito.mock(Transaction.class); + when(mockTransaction2.getGasPrice()).thenReturn(new Coin(BigInteger.valueOf(300))); + when(mockReceipt2.getTransaction()).thenReturn(mockTransaction2); + when(mockReceipt2.getGasUsed()).thenReturn(BigInteger.valueOf(300).toByteArray()); + + TransactionReceipt mockReceipt3 = Mockito.mock(TransactionReceipt.class); + Transaction mockTransaction3 = Mockito.mock(Transaction.class); + when(mockTransaction3.getGasPrice()).thenReturn(new Coin(BigInteger.valueOf(200))); + when(mockReceipt3.getTransaction()).thenReturn(mockTransaction3); + when(mockReceipt3.getGasUsed()).thenReturn(BigInteger.valueOf(200).toByteArray()); + weightedPercentileGasPriceCalculator.onBlock(mockBlock, Arrays.asList(mockReceipt1, mockReceipt2, mockReceipt3)); + + Optional gasPrice = weightedPercentileGasPriceCalculator.getGasPrice(); + assertTrue(gasPrice.isPresent(), "Gas price should be present when multiple transactions are added"); + assertEquals(new Coin(BigInteger.valueOf(200)), gasPrice.get(), "Expecting 200 as weighted percentile for the provided set."); + } + + @Test + void testCalculateGasPriceWithPlainSet() { + Block mockBlock = Mockito.mock(Block.class); + List receipts = createMockReceipts(100, 1); + weightedPercentileGasPriceCalculator.onBlock(mockBlock, receipts); + Optional gasPrice = weightedPercentileGasPriceCalculator.getGasPrice(); + assertTrue(gasPrice.isPresent(), "Gas price should be present when multiple transactions are added"); + assertEquals(new Coin(BigInteger.valueOf(25)), gasPrice.get(), "Gas price should be the weighted average of multiple transactions"); + } + + @Test + void cacheValueIsNotUpdatedUntilWindowSizeIsReached() { + WeightedPercentileCalc percentileCalc = new WeightedPercentileCalc(); + WeightedPercentileCalc spy = spy(percentileCalc); + WeightedPercentileGasPriceCalculator gasPriceCalculator = new WeightedPercentileGasPriceCalculator(spy); + + Block mockBlock = Mockito.mock(Block.class); + gasPriceCalculator.onBlock(mockBlock, createMockReceipts(10, 1)); + + Optional result1 = gasPriceCalculator.getGasPrice(); + assertTrue(result1.isPresent(), "Gas price should be present when multiple transactions are added"); + + gasPriceCalculator.onBlock(mockBlock, createMockReceipts(WINDOW_SIZE - 20, 2)); + Optional result2 = gasPriceCalculator.getGasPrice(); + assertTrue(result2.isPresent(), "Gas price should be present when multiple transactions are added"); + + assertEquals(result1.get(), result2.get(), "Gas price is not updated if window threshold is not reached"); + verify(spy, times(1)).calculateWeightedPercentile(anyFloat(), anyList()); + + gasPriceCalculator.onBlock(mockBlock, createMockReceipts(30, 1)); + Optional result3 = gasPriceCalculator.getGasPrice(); + assertTrue(result3.isPresent(), "Gas price should be present when multiple transactions are added"); + + assertNotEquals(result1.get(), result3.get(), "Gas price is updated if window threshold is reached"); + verify(spy, times(2)).calculateWeightedPercentile(anyFloat(), anyList()); + } + + @Test + void olderTxAreRemovedWhenWindowLimitIsReach() { + Block mockBlock = Mockito.mock(Block.class); + WeightedPercentileCalc mockPC = Mockito.mock(WeightedPercentileCalc.class); + when(mockPC.calculateWeightedPercentile(anyFloat(), anyList())).thenReturn(new Coin(BigInteger.valueOf(1))); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + WeightedPercentileGasPriceCalculator gpc = new WeightedPercentileGasPriceCalculator(mockPC); + + //Transactions are added until window size limit + gpc.onBlock(mockBlock, createMockReceipts(WINDOW_SIZE, 1)); + gpc.getGasPrice(); + + //New transactions are added to reach the window limit and re-calculate gas + TransactionReceipt mockReceipt = Mockito.mock(TransactionReceipt.class); + Transaction mockTransaction = Mockito.mock(Transaction.class); + when(mockTransaction.getGasPrice()).thenReturn(new Coin(BigInteger.valueOf(850))); + when(mockReceipt.getTransaction()).thenReturn(mockTransaction); + when(mockReceipt.getGasUsed()).thenReturn(BigInteger.valueOf(1).toByteArray()); + gpc.onBlock(mockBlock, Collections.singletonList(mockReceipt)); + gpc.getGasPrice(); + + verify(mockPC, times(2)).calculateWeightedPercentile(anyFloat(), captor.capture()); + List> gasPriceList = captor.getAllValues(); + + List firstList = gasPriceList.get(0); + Coin firstValueFirstList = firstList.get(0).getGasPrice(); + + assertEquals(new Coin(BigInteger.valueOf(1)), firstValueFirstList, "Gas price should be the same as the first transaction's gas price"); + + List secondList = gasPriceList.get(1); + //The second time the getGasPrice is called the first transaction should be removed and the new one added at the bottom + assertEquals(new Coin(BigInteger.valueOf(850)), secondList.get(secondList.size() - 1).getGasPrice(), "Gas price should be the same as the first transaction's gas price"); + assertEquals(firstList.subList(1, firstList.size() - 1), secondList.subList(0, secondList.size() - 2), "The first list should be the same as the second list without the first and last element"); + } + + private List createMockReceipts(int numOfReceipts, int gasUsed) { + List receipts = new ArrayList<>(); + for (int i = 0; i < numOfReceipts; i++) { + TransactionReceipt mockReceipt = Mockito.mock(TransactionReceipt.class); + Transaction mockTransaction = Mockito.mock(Transaction.class); + when(mockTransaction.getGasPrice()).thenReturn(new Coin(BigInteger.valueOf(1 + i))); + when(mockReceipt.getTransaction()).thenReturn(mockTransaction); + when(mockReceipt.getGasUsed()).thenReturn(BigInteger.valueOf(gasUsed).toByteArray()); + receipts.add(mockReceipt); + } + return receipts; + } + +} \ No newline at end of file