mirror of https://github.com/hyperledger/besu
parent
2d0732a768
commit
b55b076a91
@ -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; |
||||
} |
||||
} |
@ -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(); |
||||
} |
@ -0,0 +1,182 @@ |
||||
/* |
||||
* 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 org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
import static org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider.createInMemoryBlockchain; |
||||
import static org.mockito.ArgumentMatchers.anyLong; |
||||
import static org.mockito.ArgumentMatchers.eq; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
import org.hyperledger.besu.config.experimental.ExperimentalEIPs; |
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; |
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; |
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters; |
||||
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.FeeHistoryResult; |
||||
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.ImmutableFeeHistoryResult; |
||||
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; |
||||
import org.hyperledger.besu.ethereum.chain.MutableBlockchain; |
||||
import org.hyperledger.besu.ethereum.core.Block; |
||||
import org.hyperledger.besu.ethereum.core.BlockDataGenerator; |
||||
import org.hyperledger.besu.ethereum.core.fees.EIP1559; |
||||
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; |
||||
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; |
||||
import org.hyperledger.besu.ethereum.storage.keyvalue.WorldStateKeyValueStorage; |
||||
import org.hyperledger.besu.ethereum.storage.keyvalue.WorldStatePreimageKeyValueStorage; |
||||
import org.hyperledger.besu.ethereum.worldstate.DefaultWorldStateArchive; |
||||
import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage; |
||||
|
||||
import java.util.List; |
||||
import java.util.Optional; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
|
||||
public class EthFeeHistoryTest { |
||||
final BlockDataGenerator gen = new BlockDataGenerator(); |
||||
private MutableBlockchain blockchain; |
||||
private BlockchainQueries blockchainQueries; |
||||
private EthFeeHistory method; |
||||
private ProtocolSchedule protocolSchedule; |
||||
|
||||
@Before |
||||
public void setUp() { |
||||
protocolSchedule = mock(ProtocolSchedule.class); |
||||
final Block genesisBlock = gen.genesisBlock(); |
||||
blockchain = createInMemoryBlockchain(genesisBlock); |
||||
gen.blockSequence(genesisBlock, 10) |
||||
.forEach(block -> blockchain.appendBlock(block, gen.receipts(block))); |
||||
blockchainQueries = |
||||
new BlockchainQueries( |
||||
blockchain, |
||||
new DefaultWorldStateArchive( |
||||
new WorldStateKeyValueStorage(new InMemoryKeyValueStorage()), |
||||
new WorldStatePreimageKeyValueStorage(new InMemoryKeyValueStorage()))); |
||||
method = new EthFeeHistory(protocolSchedule, blockchainQueries); |
||||
} |
||||
|
||||
@Test |
||||
public void params() { |
||||
final ProtocolSpec londonSpec = mock(ProtocolSpec.class); |
||||
when(londonSpec.getEip1559()).thenReturn(Optional.of(new EIP1559(5))); |
||||
when(protocolSchedule.getByBlockNumber(anyLong())).thenReturn(londonSpec); |
||||
// should fail because no required params given
|
||||
assertThatThrownBy(this::feeHistoryRequest).isInstanceOf(InvalidJsonRpcParameters.class); |
||||
// should fail because newestBlock not given
|
||||
assertThatThrownBy(() -> feeHistoryRequest(1)).isInstanceOf(InvalidJsonRpcParameters.class); |
||||
// should fail because blockCount not given
|
||||
assertThatThrownBy(() -> feeHistoryRequest("latest")) |
||||
.isInstanceOf(InvalidJsonRpcParameters.class); |
||||
// should pass because both required params given
|
||||
feeHistoryRequest(1, "latest"); |
||||
// should pass because both required params and optional param given
|
||||
feeHistoryRequest(1, "latest", new double[] {1, 20.4}); |
||||
} |
||||
|
||||
@Test |
||||
public void allFieldsPresentForLatestBlock() { |
||||
final ProtocolSpec londonSpec = mock(ProtocolSpec.class); |
||||
when(londonSpec.getEip1559()).thenReturn(Optional.of(new EIP1559(5))); |
||||
when(protocolSchedule.getByBlockNumber(eq(11L))).thenReturn(londonSpec); |
||||
assertThat( |
||||
((JsonRpcSuccessResponse) feeHistoryRequest(1, "latest", new double[] {100.0})) |
||||
.getResult()) |
||||
.isEqualTo( |
||||
ImmutableFeeHistoryResult.builder() |
||||
.oldestBlock(10) |
||||
.baseFeePerGas(List.of(25496L, 28683L)) |
||||
.gasUsedRatio(List.of(0.9999999992132459)) |
||||
.reward(List.of(List.of(1524763764L))) |
||||
.build()); |
||||
} |
||||
|
||||
@Test |
||||
public void cantGetBlockHigherThanChainHead() { |
||||
final ProtocolSpec londonSpec = mock(ProtocolSpec.class); |
||||
when(londonSpec.getEip1559()).thenReturn(Optional.of(new EIP1559(5))); |
||||
when(protocolSchedule.getByBlockNumber(anyLong())).thenReturn(londonSpec); |
||||
assertThat(((JsonRpcErrorResponse) feeHistoryRequest(2, "11", new double[] {100.0})).getError()) |
||||
.isEqualTo(JsonRpcError.INVALID_PARAMS); |
||||
} |
||||
|
||||
@Test |
||||
public void blockCountBounds() { |
||||
final ProtocolSpec londonSpec = mock(ProtocolSpec.class); |
||||
when(londonSpec.getEip1559()).thenReturn(Optional.of(new EIP1559(5))); |
||||
when(protocolSchedule.getByBlockNumber(anyLong())).thenReturn(londonSpec); |
||||
assertThat( |
||||
((JsonRpcErrorResponse) feeHistoryRequest(0, "latest", new double[] {100.0})) |
||||
.getError()) |
||||
.isEqualTo(JsonRpcError.INVALID_PARAMS); |
||||
assertThat( |
||||
((JsonRpcErrorResponse) feeHistoryRequest(1025, "latest", new double[] {100.0})) |
||||
.getError()) |
||||
.isEqualTo(JsonRpcError.INVALID_PARAMS); |
||||
} |
||||
|
||||
@Test |
||||
public void doesntGoPastChainHeadWithHighBlockCount() { |
||||
final ProtocolSpec londonSpec = mock(ProtocolSpec.class); |
||||
when(londonSpec.getEip1559()).thenReturn(Optional.of(new EIP1559(5))); |
||||
when(protocolSchedule.getByBlockNumber(anyLong())).thenReturn(londonSpec); |
||||
final FeeHistoryResult result = |
||||
(ImmutableFeeHistoryResult) |
||||
((JsonRpcSuccessResponse) feeHistoryRequest(20, "latest")).getResult(); |
||||
assertThat(result.getOldestBlock()).isEqualTo(0); |
||||
assertThat(result.getBaseFeePerGas()).hasSize(12); |
||||
assertThat(result.getGasUsedRatio()).hasSize(11); |
||||
assertThat(result.getReward()).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
public void correctlyHandlesForkBlock() { |
||||
final ProtocolSpec londonSpec = mock(ProtocolSpec.class); |
||||
when(londonSpec.getEip1559()).thenReturn(Optional.of(new EIP1559(11))); |
||||
when(protocolSchedule.getByBlockNumber(anyLong())).thenReturn(londonSpec); |
||||
final FeeHistoryResult result = |
||||
(FeeHistoryResult) ((JsonRpcSuccessResponse) feeHistoryRequest(1, "latest")).getResult(); |
||||
assertThat(result.getBaseFeePerGas().get(1)) |
||||
.isEqualTo(ExperimentalEIPs.EIP1559_BASEFEE_DEFAULT_VALUE); |
||||
} |
||||
|
||||
@Test |
||||
public void allZeroPercentilesForZeroBlock() { |
||||
final ProtocolSpec londonSpec = mock(ProtocolSpec.class); |
||||
when(londonSpec.getEip1559()).thenReturn(Optional.of(new EIP1559(5))); |
||||
when(protocolSchedule.getByBlockNumber(anyLong())).thenReturn(londonSpec); |
||||
final BlockDataGenerator.BlockOptions blockOptions = BlockDataGenerator.BlockOptions.create(); |
||||
blockOptions.hasTransactions(false); |
||||
blockOptions.setParentHash(blockchain.getChainHeadHash()); |
||||
blockOptions.setBlockNumber(11); |
||||
final Block emptyBlock = gen.block(blockOptions); |
||||
blockchain.appendBlock(emptyBlock, gen.receipts(emptyBlock)); |
||||
final FeeHistoryResult result = |
||||
(FeeHistoryResult) |
||||
((JsonRpcSuccessResponse) feeHistoryRequest(1, "latest", new double[] {100.0})) |
||||
.getResult(); |
||||
assertThat(result.getReward()).isEqualTo(List.of(List.of(0L))); |
||||
} |
||||
|
||||
private JsonRpcResponse feeHistoryRequest(final Object... params) { |
||||
return method.response( |
||||
new JsonRpcRequestContext(new JsonRpcRequest("2.0", "eth_feeHistory", params))); |
||||
} |
||||
} |
@ -0,0 +1,45 @@ |
||||
{ |
||||
"request": { |
||||
"id": 28, |
||||
"jsonrpc": "2.0", |
||||
"method": "eth_feeHistory", |
||||
"params": [ |
||||
2, |
||||
"latest", |
||||
[ |
||||
0.0, |
||||
100.0, |
||||
4.0 |
||||
] |
||||
] |
||||
}, |
||||
"response": { |
||||
"jsonrpc": "2.0", |
||||
"id": 28, |
||||
"result": { |
||||
"oldestBlock": 31, |
||||
"baseFeePerGas": [ |
||||
0, |
||||
0, |
||||
0 |
||||
], |
||||
"gasUsedRatio": [ |
||||
0.00773588677333021, |
||||
0.007545537421791245 |
||||
], |
||||
"reward": [ |
||||
[ |
||||
1, |
||||
1, |
||||
1 |
||||
], |
||||
[ |
||||
1, |
||||
1, |
||||
1 |
||||
] |
||||
] |
||||
} |
||||
}, |
||||
"statusCode": 200 |
||||
} |
@ -0,0 +1,28 @@ |
||||
{ |
||||
"request": { |
||||
"id": 28, |
||||
"jsonrpc": "2.0", |
||||
"method": "eth_feeHistory", |
||||
"params": [ |
||||
2, |
||||
"latest" |
||||
] |
||||
}, |
||||
"response": { |
||||
"jsonrpc": "2.0", |
||||
"id": 28, |
||||
"result": { |
||||
"oldestBlock": 31, |
||||
"baseFeePerGas": [ |
||||
0, |
||||
0, |
||||
0 |
||||
], |
||||
"gasUsedRatio": [ |
||||
0.00773588677333021, |
||||
0.007545537421791245 |
||||
] |
||||
} |
||||
}, |
||||
"statusCode": 200 |
||||
} |
Loading…
Reference in new issue