Skip to content

Commit

Permalink
eth_feeHistory (#2466)
Browse files Browse the repository at this point in the history

Signed-off-by: Ratan Rai Sur <ratan.r.sur@gmail.com>
  • Loading branch information
RatanRSur authored Jun 22, 2021
1 parent 2d0732a commit b55b076
Show file tree
Hide file tree
Showing 19 changed files with 618 additions and 91 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## 21.7.0-RC2

### Additions and Improvements
- eth_feeHistory API for wallet providers [\#2466](https://github.com/hyperledger/besu/pull/2466)
### Bug Fixes
- Ibft2 could create invalid RoundChange messages in some circumstances containing duplicate prepares [\#2449](https://github.com/hyperledger/besu/pull/2449)

Expand Down
18 changes: 10 additions & 8 deletions besu/src/test/java/org/hyperledger/besu/PrivacyReorgTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ public class PrivacyReorgTest {
Bytes.fromBase64String("A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=");

private static final String FIRST_BLOCK_WITH_NO_TRANSACTIONS_STATE_ROOT =
"0xe368938a01d983e331eb0e4ea61224726d06075c1ad525569b369f664067ff26";
"0xb0784ff11dffceac824188583f20f3b8bb4ca275e033b3b1c0e280915743be7f";
private static final String FIRST_BLOCK_WITH_SINGLE_TRANSACTION_STATE_ROOT =
"0x9c88988f9602184efc538cf1c2f482a6b8757ff918d234602884dc8e3b983edd";
"0xe33629724501c0bc271a2b6858da64d3e92048d7e0cd019c5646770330694ff4";
private static final String BLOCK_WITH_SINGLE_TRANSACTION_RECEIPTS_ROOT =
"0xc8267b3f9ed36df3ff8adb51a6d030716f23eeb50270e7fce8d9822ffa7f0461";
private static final String STATE_ROOT_AFTER_TRANSACTION_APPENDED_TO_EMPTY_STATE =
Expand Down Expand Up @@ -221,7 +221,7 @@ public void privacyGroupHeadIsTracked() {
.contains(expected);

final String secondBlockStateRoot =
"0xd86a520e49caf215e7e4028262924db50540a5b26e415ab7c944e46a0c01d704";
"0x57a19f52a9ff4405428b3e605e136662d5c1b6be6f84f98f3b8c42ddac5139c2";
final Block secondBlock =
gen.block(getBlockOptionsNoTransaction(firstBlock, secondBlockStateRoot));

Expand Down Expand Up @@ -278,7 +278,7 @@ public void reorgToShorterChain() {
gen.block(getBlockOptionsNoTransaction(blockchain.getGenesisBlock(), firstBlockStateRoot));

final String secondBlockStateRoot =
"0x7e887f91d2a6205f4a643701aba022c2db0bac5ab235102ab7477edd7a8a4317";
"0xb3d70bce4428fb9b4549240b2130c4eea12c4ea36ae13108ed21289366a8d65f";
final Block secondBlock =
gen.block(
getBlockOptionsWithTransaction(
Expand All @@ -305,7 +305,7 @@ public void reorgToShorterChain() {
.plus(blockchain.getBlockByNumber(2).get().getHeader().getDifficulty());

final String forkBlockStateRoot =
"0x486b886bde6472e8d706f8eb4fb6378ebbdceb4848a5a8d69a726575b22e41b6";
"0x5c0adcdde38d38b4365c238c4ba05bf9ebfdf506f749b884b67003f375e43e4b";
final Block forkBlock =
gen.block(
getBlockOptionsNoTransactionWithDifficulty(
Expand Down Expand Up @@ -355,7 +355,7 @@ public void reorgToLongerChain() {
privateStateRootResolver, blockchain, STATE_ROOT_AFTER_TRANSACTION_APPENDED_TO_EMPTY_STATE);

final String secondForkBlockStateRoot =
"0x2c37a360a700c614b10c980138f64be9ad66fc4a14cd5145199cd0d8ec43d51d";
"0x76b8747d05fabcc87b2cfba519da0b1359b29fe7553bfd4837746edf31fb95fc";
final Block secondForkBlock =
gen.block(
getBlockOptionsNoTransactionWithDifficulty(
Expand All @@ -373,7 +373,7 @@ public void reorgToLongerChain() {

// Add another private transaction
final String thirdForkBlockStateRoot =
"0x8fe42678733e6099e7b10b9b1d4684b8f2ce3d6479cb122ea12932ef304a1793";
"0xcae9fa05107c1501c1962239f729d2f34186414abbaeb0dd1a3e0a6c899f79a3";
final Block thirdForkBlock =
gen.block(
getBlockOptionsWithTransactionAndDifficulty(
Expand All @@ -385,7 +385,8 @@ public void reorgToLongerChain() {
appendBlock(besuController, blockchain, protocolContext, thirdForkBlock);

// Check that the private state did change after reorg
assertPrivateStateRoot(privateStateRootResolver, blockchain, EMPTY_ROOT_HASH);
assertPrivateStateRoot(
privateStateRootResolver, blockchain, STATE_ROOT_AFTER_TRANSACTION_APPENDED_TO_EMPTY_STATE);
}

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -532,6 +533,7 @@ private BlockDataGenerator.BlockOptions getBlockOptionsWithTransactionAndDifficu
private BlockDataGenerator.BlockOptions getBlockOptions(
final BlockDataGenerator.BlockOptions blockOptions, final Block parentBlock) {
return blockOptions
.setBaseFee(Optional.empty())
.setBlockNumber(parentBlock.getHeader().getNumber() + 1)
.setParentHash(parentBlock.getHash())
.hasOmmers(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.hyperledger.besu.ethereum.core.TransactionReceipt;
import org.hyperledger.besu.ethereum.core.TransactionTestFixture;
import org.hyperledger.besu.ethereum.core.Wei;
import org.hyperledger.besu.ethereum.core.fees.EIP1559;
import org.hyperledger.besu.ethereum.eth.manager.EthContext;
import org.hyperledger.besu.ethereum.eth.manager.EthMessages;
import org.hyperledger.besu.ethereum.eth.manager.EthPeer;
Expand Down Expand Up @@ -150,7 +151,7 @@ public void setUp() {
syncState,
Wei.ZERO,
txPoolConfig,
Optional.empty());
Optional.of(new EIP1559(0)));

serviceImpl = new BesuEventsImpl(blockchain, blockBroadcaster, transactionPool, syncState);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public enum RpcMethod {
ETH_CHAIN_ID("eth_chainId"),
ETH_COINBASE("eth_coinbase"),
ETH_ESTIMATE_GAS("eth_estimateGas"),
ETH_FEE_HISTORY("eth_feeHistory"),
ETH_GAS_PRICE("eth_gasPrice"),
ETH_GET_BALANCE("eth_getBalance"),
ETH_GET_BLOCK_BY_HASH("eth_getBlockByHash"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;

import static java.util.stream.Collectors.toUnmodifiableList;

import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameter;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.ImmutableFeeHistoryResult;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.LongStream;
import java.util.stream.Stream;

public class EthFeeHistory implements JsonRpcMethod {
private final ProtocolSchedule protocolSchedule;
private final BlockchainQueries blockchainQueries;
private final Blockchain blockchain;

public EthFeeHistory(
final ProtocolSchedule protocolSchedule, final BlockchainQueries blockchainQueries) {
this.protocolSchedule = protocolSchedule;
this.blockchainQueries = blockchainQueries;
this.blockchain = blockchainQueries.getBlockchain();
}

@Override
public String getName() {
return RpcMethod.ETH_FEE_HISTORY.getMethodName();
}

@Override
public JsonRpcResponse response(final JsonRpcRequestContext request) {
final Object requestId = request.getRequest().getId();

final long blockCount = request.getRequiredParameter(0, Long.class);
if (blockCount < 1 || blockCount > 1024) {
return new JsonRpcErrorResponse(requestId, JsonRpcError.INVALID_PARAMS);
}
final BlockParameter highestBlock = request.getRequiredParameter(1, BlockParameter.class);
final Optional<List<Double>> maybeRewardPercentiles =
request.getOptionalParameter(2, Double[].class).map(Arrays::asList);

final long chainHeadBlockNumber = blockchain.getChainHeadBlockNumber();
final long resolvedHighestBlockNumber =
highestBlock
.getNumber()
.orElse(
chainHeadBlockNumber /* both latest and pending use the head block until we have pending block support */);

if (resolvedHighestBlockNumber > chainHeadBlockNumber) {
return new JsonRpcErrorResponse(requestId, JsonRpcError.INVALID_PARAMS);
}

final long oldestBlock = Math.max(0, resolvedHighestBlockNumber - (blockCount - 1));

final List<BlockHeader> blockHeaders =
LongStream.range(oldestBlock, oldestBlock + blockCount)
.mapToObj(blockchain::getBlockHeader)
.flatMap(Optional::stream)
.collect(toUnmodifiableList());

// we return the base fees for the blocks requested and 1 more because we can always compute it
final List<Long> explicitlyRequestedBaseFees =
blockHeaders.stream()
.map(blockHeader -> blockHeader.getBaseFee().orElse(0L))
.collect(toUnmodifiableList());
final long nextBlockNumber = resolvedHighestBlockNumber + 1;
final Long nextBaseFee =
blockchain
.getBlockHeader(nextBlockNumber)
.map(blockHeader -> blockHeader.getBaseFee().orElse(0L))
.orElseGet(
() ->
protocolSchedule
.getByBlockNumber(nextBlockNumber)
.getEip1559()
.map(
eip1559 -> {
final BlockHeader lastBlockHeader =
blockHeaders.get(blockHeaders.size() - 1);
return eip1559.computeBaseFee(
nextBlockNumber,
explicitlyRequestedBaseFees.get(
explicitlyRequestedBaseFees.size() - 1),
lastBlockHeader.getGasUsed(),
eip1559.targetGasUsed(lastBlockHeader));
})
.orElse(0L));

final List<Double> gasUsedRatios =
blockHeaders.stream()
.map(blockHeader -> blockHeader.getGasUsed() / (double) blockHeader.getGasLimit())
.collect(toUnmodifiableList());

final Optional<List<List<Long>>> maybeRewards =
maybeRewardPercentiles.map(
rewardPercentiles ->
LongStream.range(oldestBlock, oldestBlock + blockCount)
.mapToObj(blockchain::getBlockByNumber)
.flatMap(Optional::stream)
.map(
block ->
computeRewards(
rewardPercentiles.stream().sorted().collect(toUnmodifiableList()),
block))
.collect(toUnmodifiableList()));

final ImmutableFeeHistoryResult.Builder feeHistoryResultBuilder =
ImmutableFeeHistoryResult.builder()
.oldestBlock(oldestBlock)
.baseFeePerGas(
Stream.concat(explicitlyRequestedBaseFees.stream(), Stream.of(nextBaseFee))
.collect(toUnmodifiableList()))
.gasUsedRatio(gasUsedRatios);
maybeRewards.ifPresent(feeHistoryResultBuilder::reward);
return new JsonRpcSuccessResponse(requestId, feeHistoryResultBuilder.build());
}

private List<Long> computeRewards(
final List<Double> rewardPercentiles, final org.hyperledger.besu.ethereum.core.Block block) {
final List<Transaction> transactions = block.getBody().getTransactions();
if (transactions.isEmpty()) {
// all 0's for empty block
return LongStream.generate(() -> 0)
.limit(rewardPercentiles.size())
.boxed()
.collect(toUnmodifiableList());
}

final Optional<Long> baseFee = block.getHeader().getBaseFee();
final List<Transaction> transactionsAscendingEffectiveGasFee =
transactions.stream()
.sorted(
Comparator.comparing(
transaction -> transaction.getEffectivePriorityFeePerGas(baseFee)))
.collect(toUnmodifiableList());

// We need to weight the percentile of rewards by the gas used in the transaction.
// That's why we're keeping track of the cumulative gas used and checking to see which
// percentile markers we've passed
final ArrayList<Long> rewards = new ArrayList<>();
int rewardPercentileIndex = 0;
long gasUsed = 0;
for (final Transaction transaction : transactionsAscendingEffectiveGasFee) {

gasUsed +=
blockchainQueries
.transactionReceiptByTransactionHash(transaction.getHash())
.get()
.getGasUsed();

while (rewardPercentileIndex < rewardPercentiles.size()
&& 100.0 * gasUsed / block.getHeader().getGasUsed()
>= rewardPercentiles.get(rewardPercentileIndex)) {
rewards.add(transaction.getEffectivePriorityFeePerGas(baseFee));
rewardPercentileIndex++;
}
}
return rewards;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.results;

import java.util.List;
import javax.annotation.Nullable;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.immutables.value.Value;

@Value.Immutable
@JsonInclude(JsonInclude.Include.NON_NULL)
public interface FeeHistoryResult {

@JsonProperty("oldestBlock")
long getOldestBlock();

@JsonProperty("baseFeePerGas")
List<Long> getBaseFeePerGas();

@JsonProperty("gasUsedRatio")
List<Double> getGasUsedRatio();

@Nullable
@JsonProperty("reward")
List<List<Long>> getReward();
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthChainId;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthCoinbase;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthEstimateGas;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthFeeHistory;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthGasPrice;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthGetBalance;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.EthGetBlockByHash;
Expand Down Expand Up @@ -128,6 +129,7 @@ protected Map<String, JsonRpcMethod> create() {
blockchainQueries.getWorldStateArchive(),
protocolSchedule,
privacyParameters)),
new EthFeeHistory(protocolSchedule, blockchainQueries),
new EthGetCode(blockchainQueries, Optional.of(privacyParameters)),
new EthGetLogs(blockchainQueries),
new EthGetProof(blockchainQueries),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1699,8 +1699,13 @@ private void assertTransactionResultMatchesTransaction(
assertThat(result.getValue("to")).isNull();
}
assertThat(Wei.fromHexString(result.getString("value"))).isEqualTo(transaction.getValue());
assertThat(Wei.fromHexString(result.getString("gasPrice")))
.isEqualTo(transaction.getGasPrice().get());
assertThat(Optional.ofNullable(result.getString("gasPrice")).map(Wei::fromHexString))
.isEqualTo(transaction.getGasPrice());
assertThat(Optional.ofNullable(result.getString("maxFeePerGas")).map(Wei::fromHexString))
.isEqualTo(transaction.getMaxFeePerGas());
assertThat(
Optional.ofNullable(result.getString("maxPriorityFeePerGas")).map(Wei::fromHexString))
.isEqualTo(transaction.getMaxPriorityFeePerGas());
assertThat(Long.decode(result.getString("gas"))).isEqualTo(transaction.getGasLimit());
assertThat(Bytes.fromHexString(result.getString("input"))).isEqualTo(transaction.getPayload());
}
Expand Down
Loading

0 comments on commit b55b076

Please sign in to comment.