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