Fix eth_feeHistory rewards when bounded by configuration (#7750)

Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net>
pull/7762/head
Fabio Di Fabio 2 months ago committed by GitHub
parent 174d4281da
commit d5ee9b7bb1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 45
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthFeeHistory.java
  3. 6
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/EthJsonRpcMethods.java
  4. 67
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthFeeHistoryTest.java

@ -9,6 +9,7 @@
### Additions and Improvements ### Additions and Improvements
### Bug fixes ### Bug fixes
- Fix eth_feeHistory rewards when bounded by configuration [#7750](https://github.com/hyperledger/besu/pull/7750)
## 24.10.0 ## 24.10.0

@ -14,7 +14,6 @@
*/ */
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;
import static java.util.stream.Collectors.toUnmodifiableList;
import static org.hyperledger.besu.ethereum.mainnet.feemarket.ExcessBlobGasCalculator.calculateExcessBlobGasForParent; import static org.hyperledger.besu.ethereum.mainnet.feemarket.ExcessBlobGasCalculator.calculateExcessBlobGasForParent;
import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.Hash;
@ -32,6 +31,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSucces
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.FeeHistory; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.FeeHistory;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.ImmutableFeeHistory; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.ImmutableFeeHistory;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator; import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.core.Block;
@ -57,9 +57,11 @@ import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Streams; import com.google.common.collect.Streams;
import org.apache.tuweni.units.bigints.UInt256s;
public class EthFeeHistory implements JsonRpcMethod { public class EthFeeHistory implements JsonRpcMethod {
private final ProtocolSchedule protocolSchedule; private final ProtocolSchedule protocolSchedule;
private final BlockchainQueries blockchainQueries;
private final Blockchain blockchain; private final Blockchain blockchain;
private final MiningCoordinator miningCoordinator; private final MiningCoordinator miningCoordinator;
private final ApiConfiguration apiConfiguration; private final ApiConfiguration apiConfiguration;
@ -70,13 +72,14 @@ public class EthFeeHistory implements JsonRpcMethod {
public EthFeeHistory( public EthFeeHistory(
final ProtocolSchedule protocolSchedule, final ProtocolSchedule protocolSchedule,
final Blockchain blockchain, final BlockchainQueries blockchainQueries,
final MiningCoordinator miningCoordinator, final MiningCoordinator miningCoordinator,
final ApiConfiguration apiConfiguration) { final ApiConfiguration apiConfiguration) {
this.protocolSchedule = protocolSchedule; this.protocolSchedule = protocolSchedule;
this.blockchain = blockchain; this.blockchainQueries = blockchainQueries;
this.miningCoordinator = miningCoordinator; this.miningCoordinator = miningCoordinator;
this.apiConfiguration = apiConfiguration; this.apiConfiguration = apiConfiguration;
this.blockchain = blockchainQueries.getBlockchain();
this.cache = Caffeine.newBuilder().maximumSize(MAXIMUM_CACHE_SIZE).build(); this.cache = Caffeine.newBuilder().maximumSize(MAXIMUM_CACHE_SIZE).build();
} }
@ -136,7 +139,8 @@ public class EthFeeHistory implements JsonRpcMethod {
final List<Double> gasUsedRatios = getGasUsedRatios(blockHeaderRange); final List<Double> gasUsedRatios = getGasUsedRatios(blockHeaderRange);
final List<Double> blobGasUsedRatios = getBlobGasUsedRatios(blockHeaderRange); final List<Double> blobGasUsedRatios = getBlobGasUsedRatios(blockHeaderRange);
final Optional<List<List<Wei>>> maybeRewards = final Optional<List<List<Wei>>> maybeRewards =
maybeRewardPercentiles.map(rewards -> getRewards(rewards, blockHeaderRange)); maybeRewardPercentiles.map(
percentiles -> getRewards(percentiles, blockHeaderRange, nextBaseFee));
return new JsonRpcSuccessResponse( return new JsonRpcSuccessResponse(
requestId, requestId,
createFeeHistoryResult( createFeeHistoryResult(
@ -203,17 +207,19 @@ public class EthFeeHistory implements JsonRpcMethod {
} }
private List<List<Wei>> getRewards( private List<List<Wei>> getRewards(
final List<Double> rewardPercentiles, final List<BlockHeader> blockHeaders) { final List<Double> rewardPercentiles,
final List<BlockHeader> blockHeaders,
final Wei nextBaseFee) {
var sortedPercentiles = rewardPercentiles.stream().sorted().toList(); var sortedPercentiles = rewardPercentiles.stream().sorted().toList();
return blockHeaders.stream() return blockHeaders.stream()
.parallel() .parallel()
.map(blockHeader -> calculateBlockHeaderReward(sortedPercentiles, blockHeader)) .map(blockHeader -> calculateBlockHeaderReward(sortedPercentiles, blockHeader, nextBaseFee))
.flatMap(Optional::stream) .flatMap(Optional::stream)
.toList(); .toList();
} }
private Optional<List<Wei>> calculateBlockHeaderReward( private Optional<List<Wei>> calculateBlockHeaderReward(
final List<Double> sortedPercentiles, final BlockHeader blockHeader) { final List<Double> sortedPercentiles, final BlockHeader blockHeader, final Wei nextBaseFee) {
// Create a new key for the reward cache // Create a new key for the reward cache
final RewardCacheKey key = new RewardCacheKey(blockHeader.getBlockHash(), sortedPercentiles); final RewardCacheKey key = new RewardCacheKey(blockHeader.getBlockHash(), sortedPercentiles);
@ -226,7 +232,7 @@ public class EthFeeHistory implements JsonRpcMethod {
Optional<Block> block = blockchain.getBlockByHash(blockHeader.getBlockHash()); Optional<Block> block = blockchain.getBlockByHash(blockHeader.getBlockHash());
return block.map( return block.map(
b -> { b -> {
List<Wei> rewards = computeRewards(sortedPercentiles, b); List<Wei> rewards = computeRewards(sortedPercentiles, b, nextBaseFee);
// Put the computed rewards in the cache for future use // Put the computed rewards in the cache for future use
cache.put(key, rewards); cache.put(key, rewards);
return rewards; return rewards;
@ -237,7 +243,8 @@ public class EthFeeHistory implements JsonRpcMethod {
record TransactionInfo(Transaction transaction, Long gasUsed, Wei effectivePriorityFeePerGas) {} record TransactionInfo(Transaction transaction, Long gasUsed, Wei effectivePriorityFeePerGas) {}
@VisibleForTesting @VisibleForTesting
public List<Wei> computeRewards(final List<Double> rewardPercentiles, final Block block) { public List<Wei> computeRewards(
final List<Double> rewardPercentiles, final Block block, final Wei nextBaseFee) {
final List<Transaction> transactions = block.getBody().getTransactions(); final List<Transaction> transactions = block.getBody().getTransactions();
if (transactions.isEmpty()) { if (transactions.isEmpty()) {
// all 0's for empty block // all 0's for empty block
@ -253,7 +260,7 @@ public class EthFeeHistory implements JsonRpcMethod {
// If the priority fee boundary is set, return the bounded rewards. Otherwise, return the real // If the priority fee boundary is set, return the bounded rewards. Otherwise, return the real
// rewards. // rewards.
if (apiConfiguration.isGasAndPriorityFeeLimitingEnabled()) { if (apiConfiguration.isGasAndPriorityFeeLimitingEnabled()) {
return boundRewards(realRewards); return boundRewards(realRewards, nextBaseFee);
} else { } else {
return realRewards; return realRewards;
} }
@ -292,16 +299,20 @@ public class EthFeeHistory implements JsonRpcMethod {
* This method returns a list of bounded rewards. * This method returns a list of bounded rewards.
* *
* @param rewards The list of rewards to be bounded. * @param rewards The list of rewards to be bounded.
* @param nextBaseFee The base fee of the next block.
* @return The list of bounded rewards. * @return The list of bounded rewards.
*/ */
private List<Wei> boundRewards(final List<Wei> rewards) { private List<Wei> boundRewards(final List<Wei> rewards, final Wei nextBaseFee) {
Wei minPriorityFee = miningCoordinator.getMinPriorityFeePerGas(); final Wei lowerBoundGasPrice = blockchainQueries.gasPriceLowerBound();
Wei lowerBound = final Wei lowerBoundPriorityFee = lowerBoundGasPrice.subtract(nextBaseFee);
minPriorityFee final Wei minPriorityFee = miningCoordinator.getMinPriorityFeePerGas();
final Wei forcedMinPriorityFee = UInt256s.max(minPriorityFee, lowerBoundPriorityFee);
final Wei lowerBound =
forcedMinPriorityFee
.multiply(apiConfiguration.getLowerBoundGasAndPriorityFeeCoefficient()) .multiply(apiConfiguration.getLowerBoundGasAndPriorityFeeCoefficient())
.divide(100); .divide(100);
Wei upperBound = final Wei upperBound =
minPriorityFee forcedMinPriorityFee
.multiply(apiConfiguration.getUpperBoundGasAndPriorityFeeCoefficient()) .multiply(apiConfiguration.getUpperBoundGasAndPriorityFeeCoefficient())
.divide(100); .divide(100);
@ -438,7 +449,7 @@ public class EthFeeHistory implements JsonRpcMethod {
.oldestBlock(oldestBlock) .oldestBlock(oldestBlock)
.baseFeePerGas( .baseFeePerGas(
Stream.concat(explicitlyRequestedBaseFees.stream(), Stream.of(nextBaseFee)) Stream.concat(explicitlyRequestedBaseFees.stream(), Stream.of(nextBaseFee))
.collect(toUnmodifiableList())) .toList())
.baseFeePerBlobGas(requestedBlobBaseFees) .baseFeePerBlobGas(requestedBlobBaseFees)
.gasUsedRatio(gasUsedRatios) .gasUsedRatio(gasUsedRatios)
.blobGasUsedRatio(blobGasUsedRatio) .blobGasUsedRatio(blobGasUsedRatio)

@ -132,11 +132,7 @@ public class EthJsonRpcMethods extends ApiGroupJsonRpcMethods {
blockchainQueries.getWorldStateArchive(), blockchainQueries.getWorldStateArchive(),
protocolSchedule, protocolSchedule,
apiConfiguration.getGasCap())), apiConfiguration.getGasCap())),
new EthFeeHistory( new EthFeeHistory(protocolSchedule, blockchainQueries, miningCoordinator, apiConfiguration),
protocolSchedule,
blockchainQueries.getBlockchain(),
miningCoordinator,
apiConfiguration),
new EthGetCode(blockchainQueries), new EthGetCode(blockchainQueries),
new EthGetLogs(blockchainQueries, apiConfiguration.getMaxLogsRange()), new EthGetLogs(blockchainQueries, apiConfiguration.getMaxLogsRange()),
new EthGetProof(blockchainQueries), new EthGetProof(blockchainQueries),

@ -39,6 +39,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.FeeHistory; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.FeeHistory;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.ImmutableFeeHistory; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.ImmutableFeeHistory;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.ImmutableFeeHistoryResult; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.ImmutableFeeHistoryResult;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator; import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator;
import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.chain.MutableBlockchain; import org.hyperledger.besu.ethereum.chain.MutableBlockchain;
@ -69,6 +70,7 @@ import org.junit.jupiter.api.Test;
public class EthFeeHistoryTest { public class EthFeeHistoryTest {
final BlockDataGenerator gen = new BlockDataGenerator(); final BlockDataGenerator gen = new BlockDataGenerator();
private BlockchainQueries blockchainQueries;
private MutableBlockchain blockchain; private MutableBlockchain blockchain;
private EthFeeHistory method; private EthFeeHistory method;
private ProtocolSchedule protocolSchedule; private ProtocolSchedule protocolSchedule;
@ -82,14 +84,15 @@ public class EthFeeHistoryTest {
gen.blockSequence(genesisBlock, 10) gen.blockSequence(genesisBlock, 10)
.forEach(block -> blockchain.appendBlock(block, gen.receipts(block))); .forEach(block -> blockchain.appendBlock(block, gen.receipts(block)));
miningCoordinator = mock(MergeCoordinator.class); miningCoordinator = mock(MergeCoordinator.class);
when(miningCoordinator.getMinPriorityFeePerGas()).thenReturn(Wei.ONE);
blockchainQueries = mockBlockchainQueries(blockchain, Wei.of(7));
mockFork(); mockFork();
method = method =
new EthFeeHistory( new EthFeeHistory(
protocolSchedule, protocolSchedule,
blockchain, blockchainQueries,
miningCoordinator, miningCoordinator,
ImmutableApiConfiguration.builder().build()); ImmutableApiConfiguration.builder().build());
} }
@ -139,11 +142,16 @@ public class EthFeeHistoryTest {
Block block = mock(Block.class); Block block = mock(Block.class);
Blockchain blockchain = mockBlockchainTransactionsWithPriorityFee(block); Blockchain blockchain = mockBlockchainTransactionsWithPriorityFee(block);
final var blockchainQueries = mockBlockchainQueries(blockchain, Wei.of(7));
EthFeeHistory ethFeeHistory = EthFeeHistory ethFeeHistory =
new EthFeeHistory( new EthFeeHistory(
null, blockchain, miningCoordinator, ImmutableApiConfiguration.builder().build()); null,
blockchainQueries,
miningCoordinator,
ImmutableApiConfiguration.builder().build());
List<Wei> rewards = ethFeeHistory.computeRewards(rewardPercentiles, block); List<Wei> rewards = ethFeeHistory.computeRewards(rewardPercentiles, block, Wei.of(7));
// Define the expected rewards for each percentile // Define the expected rewards for each percentile
// The expected rewards match the fees of the transactions at each percentile in the // The expected rewards match the fees of the transactions at each percentile in the
@ -179,14 +187,51 @@ public class EthFeeHistoryTest {
.upperBoundGasAndPriorityFeeCoefficient(500L) .upperBoundGasAndPriorityFeeCoefficient(500L)
.build(); // Max reward = Wei.One * 500L / 100 = 5.0 .build(); // Max reward = Wei.One * 500L / 100 = 5.0
final var blockchainQueries = mockBlockchainQueries(blockchain, Wei.of(7));
when(miningCoordinator.getMinPriorityFeePerGas()).thenReturn(Wei.ONE);
EthFeeHistory ethFeeHistory = EthFeeHistory ethFeeHistory =
new EthFeeHistory(null, blockchain, miningCoordinator, apiConfiguration); new EthFeeHistory(null, blockchainQueries, miningCoordinator, apiConfiguration);
List<Wei> rewards = ethFeeHistory.computeRewards(rewardPercentiles, block); List<Wei> rewards = ethFeeHistory.computeRewards(rewardPercentiles, block, Wei.of(7));
// Define the expected bounded rewards for each percentile // Define the expected bounded rewards for each percentile
List<Wei> expectedBoundedRewards = Stream.of(2, 2, 2, 4, 5, 5, 5, 5, 5).map(Wei::of).toList(); List<Wei> expectedBoundedRewards = Stream.of(2, 2, 2, 4, 5, 5, 5, 5, 5).map(Wei::of).toList();
assertThat(expectedBoundedRewards).isEqualTo(rewards); assertThat(rewards).isEqualTo(expectedBoundedRewards);
}
@Test
public void shouldApplyLowerBoundRewardsCorrectly() {
// This test checks that the rewards are correctly bounded by the lower and upper limits,
// when the calculated lower bound for the priority fee is greater than the configured one.
// Configured minPriorityFeePerGas is 0 wei, minGasPrice is 10 wei and baseFee is 8 wei,
// so for a tx to be mined the minPriorityFeePerGas is raised to 2 wei before applying the
// coefficients.
List<Double> rewardPercentiles =
Arrays.asList(0.0, 5.0, 10.0, 27.50, 31.0, 59.0, 60.0, 61.0, 100.0);
Block block = mock(Block.class);
Blockchain blockchain = mockBlockchainTransactionsWithPriorityFee(block);
ApiConfiguration apiConfiguration =
ImmutableApiConfiguration.builder()
.isGasAndPriorityFeeLimitingEnabled(true)
.lowerBoundGasAndPriorityFeeCoefficient(200L) // Min reward = 2 * 200L / 100 = 4.0
.upperBoundGasAndPriorityFeeCoefficient(300L)
.build(); // Max reward = 2 * 300L / 100 = 6.0
final var blockchainQueries = mockBlockchainQueries(blockchain, Wei.of(10));
when(miningCoordinator.getMinPriorityFeePerGas()).thenReturn(Wei.ZERO);
EthFeeHistory ethFeeHistory =
new EthFeeHistory(null, blockchainQueries, miningCoordinator, apiConfiguration);
List<Wei> rewards = ethFeeHistory.computeRewards(rewardPercentiles, block, Wei.of(8));
// Define the expected bounded rewards for each percentile
List<Wei> expectedBoundedRewards = Stream.of(4, 4, 4, 4, 5, 6, 6, 6, 6).map(Wei::of).toList();
assertThat(rewards).isEqualTo(expectedBoundedRewards);
} }
private Blockchain mockBlockchainTransactionsWithPriorityFee(final Block block) { private Blockchain mockBlockchainTransactionsWithPriorityFee(final Block block) {
@ -399,4 +444,12 @@ public class EthFeeHistoryTest {
return method.response( return method.response(
new JsonRpcRequestContext(new JsonRpcRequest("2.0", "eth_feeHistory", params))); new JsonRpcRequestContext(new JsonRpcRequest("2.0", "eth_feeHistory", params)));
} }
private BlockchainQueries mockBlockchainQueries(
final Blockchain blockchain, final Wei gasPriceLowerBound) {
final var blockchainQueries = mock(BlockchainQueries.class);
when(blockchainQueries.getBlockchain()).thenReturn(blockchain);
when(blockchainQueries.gasPriceLowerBound()).thenReturn(gasPriceLowerBound);
return blockchainQueries;
}
} }

Loading…
Cancel
Save