Skip to content

Commit

Permalink
Implement eth_feeHistory (ethereumjs#3295)
Browse files Browse the repository at this point in the history
* implement effective priority fee retrieval

implement fee history rpc method

refactor and adjust return values

add integer validation

add test for maximum base fee increase

fix backward compatibility

add remaining tests

use calcNextBaseFee from parent block header

use bigint array instead of number array for the optional param

remove redundant bigIntMax

retrieve initial base fee from common

* client: fix build

* client: partial fix tests

* client: edit feeHistory tests

* client/tx address some review

* tx: make getEffectivePriorityFee redunt

* tx: add getEffectivePriorityFee tests

* vm: add todo

* client: eth_feeHistory fixes + test additions

* client/rpc: add rewardPrcentile check

* client: add validation tests for the ratio

* client: eth_feeHistory fix rewards?

* Add partial tests for reward percentiles

* client: feeHistory sort txs by prioFee

* add more tests

* client: add extra feeHistory rewards tests

* vm: use getEffectivePriorityFee

* client: update mock blockhash

* add blob fee to feeHistory

* block: add calcNextBlobGasPrice

* client: fix feeHistory implementation and add test output

* client: lint

* separate validators for rewardPercentile and array

* client: test rewardPercentile validator

* Apply comments and add tests

---------

Co-authored-by: Marko <[email protected]>
Co-authored-by: acolytec3 <[email protected]>
Co-authored-by: ScottyPoi <[email protected]>
Co-authored-by: Scotty <[email protected]>
  • Loading branch information
5 people authored Mar 7, 2024
1 parent d8bd18b commit a35bf07
Show file tree
Hide file tree
Showing 21 changed files with 920 additions and 7 deletions.
18 changes: 17 additions & 1 deletion packages/block/src/header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,9 +597,17 @@ export class BlockHeader {
if (this.excessBlobGas === undefined) {
throw new Error('header must have excessBlobGas field populated')
}
return this._getBlobGasPrice(this.excessBlobGas)
}

/**
* Returns the blob gas price depending upon the `excessBlobGas` value
* @param excessBlobGas
*/
private _getBlobGasPrice(excessBlobGas: bigint) {
return fakeExponential(
this.common.param('gasPrices', 'minBlobGasPrice'),
this.excessBlobGas,
excessBlobGas,
this.common.param('gasConfig', 'blobGasPriceUpdateFraction')
)
}
Expand Down Expand Up @@ -633,6 +641,14 @@ export class BlockHeader {
}
}

/**
* Calculate the blob gas price of the block built on top of this one
* @returns The blob gas price
*/
public calcNextBlobGasPrice(): bigint {
return this._getBlobGasPrice(this.calcNextExcessBlobGas())
}

/**
* Returns a Uint8Array Array of the raw Bytes in this header, in order.
*/
Expand Down
3 changes: 3 additions & 0 deletions packages/block/test/eip4844block.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ describe('blob gas tests', () => {
assert.equal(lowGasHeader.calcDataFee(1), 131072n, 'compute data fee correctly')
assert.equal(highGasHeader.calcDataFee(4), 3145728n, 'compute data fee correctly')
assert.equal(highGasHeader.calcDataFee(6), 4718592n, 'compute data fee correctly')

const nextBlobGas = highGasHeader.calcNextBlobGasPrice()
assert.equal(nextBlobGas, BigInt(7)) // TODO verify that this is correct
})
})

Expand Down
170 changes: 170 additions & 0 deletions packages/client/src/rpc/modules/eth.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { Hardfork } from '@ethereumjs/common'
import { BlobEIP4844Transaction, Capability, TransactionFactory } from '@ethereumjs/tx'
import {
Address,
BIGINT_0,
BIGINT_1,
BIGINT_100,
BIGINT_NEG1,
TypeOutput,
bigIntMax,
bigIntToHex,
bytesToHex,
equalsBytes,
Expand Down Expand Up @@ -200,6 +204,74 @@ const jsonRpcReceipt = async (
blobGasPrice: blobGasPrice !== undefined ? bigIntToHex(blobGasPrice) : undefined,
})

const calculateRewards = async (
block: Block,
receiptsManager: ReceiptsManager,
priorityFeePercentiles: number[]
) => {
if (priorityFeePercentiles.length === 0) {
return []
}
if (block.transactions.length === 0) {
return Array.from({ length: priorityFeePercentiles.length }, () => BIGINT_0)
}

const blockRewards: bigint[] = []
const txGasUsed: bigint[] = []
const baseFee = block.header.baseFeePerGas
const receipts = await receiptsManager.getReceipts(block.hash())

if (receipts.length > 0) {
txGasUsed.push(receipts[0].cumulativeBlockGasUsed)
for (let i = 1; i < receipts.length; i++) {
txGasUsed.push(receipts[i].cumulativeBlockGasUsed - receipts[i - 1].cumulativeBlockGasUsed)
}
}

const txs = block.transactions
const txsWithGasUsed = txs.map((tx, i) => ({
txGasUsed: txGasUsed[i],
// Can assume baseFee exists, since if EIP1559/EIP4844 txs are included, this is a post-EIP-1559 block.
effectivePriorityFee: tx.getEffectivePriorityFee(baseFee!),
}))

// Sort array based upon the effectivePriorityFee
txsWithGasUsed.sort((a, b) => Number(a.effectivePriorityFee - b.effectivePriorityFee))

let priorityFeeIndex = 0
// Loop over all txs ...
let targetCumulativeGasUsed =
(block.header.gasUsed * BigInt(priorityFeePercentiles[0])) / BIGINT_100
let cumulativeGasUsed = BIGINT_0
for (let txIndex = 0; txIndex < txsWithGasUsed.length; txIndex++) {
cumulativeGasUsed += txsWithGasUsed[txIndex].txGasUsed
while (
cumulativeGasUsed >= targetCumulativeGasUsed &&
priorityFeeIndex < priorityFeePercentiles.length
) {
/*
Idea: keep adding the premium fee to the priority fee percentile until we actually get above the threshold
For instance, take the priority fees [0,1,2,100]
The gas used in the block is 1.05 million
The first tx takes 1 million gas with prio fee A, the second the remainder over 0.05M with prio fee B
Then it is clear that the priority fees should be [A,A,A,B]
-> So A should be added three times
Note: in this case A < B so the priority fees were "sorted" by default
*/
blockRewards.push(txsWithGasUsed[txIndex].effectivePriorityFee)
priorityFeeIndex++
if (priorityFeeIndex >= priorityFeePercentiles.length) {
// prevent out-of-bounds read
break
}
const priorityFeePercentile = priorityFeePercentiles[priorityFeeIndex]
targetCumulativeGasUsed = (block.header.gasUsed * BigInt(priorityFeePercentile)) / BIGINT_100
}
}

return blockRewards
}

/**
* eth_* RPC module
* @memberof module:rpc/modules
Expand Down Expand Up @@ -366,6 +438,16 @@ export class Eth {
)

this.gasPrice = middleware(callWithStackTrace(this.gasPrice.bind(this), this._rpcDebug), 0, [])

this.feeHistory = middleware(
callWithStackTrace(this.feeHistory.bind(this), this._rpcDebug),
2,
[
[validators.either(validators.hex, validators.integer)],
[validators.either(validators.hex, validators.blockOption)],
[validators.rewardPercentiles],
]
)
}

/**
Expand Down Expand Up @@ -1110,4 +1192,92 @@ export class Eth {

return bigIntToHex(gasPrice)
}

async feeHistory(params: [string | number | bigint, string, [number]?]) {
const blockCount = BigInt(params[0])
const [, lastBlockRequested, priorityFeePercentiles] = params

if (blockCount < 1 || blockCount > 1024) {
throw {
code: INVALID_PARAMS,
message: 'invalid block count',
}
}

const { number: lastRequestedBlockNumber } = (
await getBlockByOption(lastBlockRequested, this._chain)
).header

const oldestBlockNumber = bigIntMax(lastRequestedBlockNumber - blockCount + BIGINT_1, BIGINT_0)

const requestedBlockNumbers = Array.from(
{ length: Number(blockCount) },
(_, i) => oldestBlockNumber + BigInt(i)
)

const requestedBlocks = await Promise.all(
requestedBlockNumbers.map((n) => getBlockByOption(n.toString(), this._chain))
)

const [baseFees, gasUsedRatios, baseFeePerBlobGas, blobGasUsedRatio] = requestedBlocks.reduce(
(v, b) => {
const [prevBaseFees, prevGasUsedRatios, prevBaseFeesPerBlobGas, prevBlobGasUsedRatio] = v
const { baseFeePerGas, gasUsed, gasLimit, blobGasUsed } = b.header

let baseFeePerBlobGas = BIGINT_0
let blobGasUsedRatio = 0
if (b.header.excessBlobGas !== undefined) {
baseFeePerBlobGas = b.header.getBlobGasPrice()
const max = b.common.param('gasConfig', 'maxblobGasPerBlock')
blobGasUsedRatio = Number(blobGasUsed) / Number(max)
}

prevBaseFees.push(baseFeePerGas ?? BIGINT_0)
prevGasUsedRatios.push(Number(gasUsed) / Number(gasLimit))

prevBaseFeesPerBlobGas.push(baseFeePerBlobGas)
prevBlobGasUsedRatio.push(blobGasUsedRatio)

return [prevBaseFees, prevGasUsedRatios, prevBaseFeesPerBlobGas, prevBlobGasUsedRatio]
},
[[], [], [], []] as [bigint[], number[], bigint[], number[]]
)

const londonHardforkBlockNumber = this._chain.blockchain.common.hardforkBlock(Hardfork.London)!
const nextBaseFee =
lastRequestedBlockNumber - londonHardforkBlockNumber >= BIGINT_NEG1
? requestedBlocks[requestedBlocks.length - 1].header.calcNextBaseFee()
: BIGINT_0
baseFees.push(nextBaseFee)

if (this._chain.blockchain.common.isActivatedEIP(4844)) {
baseFeePerBlobGas.push(
requestedBlocks[requestedBlocks.length - 1].header.calcNextBlobGasPrice()
)
} else {
// TODO (?): known bug
// If the next block is the first block where 4844 is returned, then
// BIGINT_1 should be pushed, not BIGINT_0
baseFeePerBlobGas.push(BIGINT_0)
}

let rewards: bigint[][] = []

if (this.receiptsManager && priorityFeePercentiles) {
rewards = await Promise.all(
requestedBlocks.map((b) =>
calculateRewards(b, this.receiptsManager!, priorityFeePercentiles)
)
)
}

return {
baseFeePerGas: baseFees.map(bigIntToHex),
gasUsedRatio: gasUsedRatios,
baseFeePerBlobGas: baseFeePerBlobGas.map(bigIntToHex),
blobGasUsedRatio,
oldestBlock: bigIntToHex(oldestBlockNumber),
reward: rewards.map((r) => r.map(bigIntToHex)),
}
}
}
86 changes: 86 additions & 0 deletions packages/client/src/rpc/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,23 @@ export const validators = {
}
},

/**
* number validator to check if type is integer
* @param params parameters of method
* @param index index of parameter
*/

get integer() {
return (params: any[], index: number) => {
if (!Number.isInteger(params[index])) {
return {
code: INVALID_PARAMS,
message: `invalid argument ${index}: argument is not an integer`,
}
}
}
},

/**
* validator to ensure required transaction fields are present, and checks for valid address and hex values.
* @param requiredFields array of required fields
Expand Down Expand Up @@ -464,6 +481,75 @@ export const validators = {
}
},

/**
* Verification of rewardPercentile value
*
* description: Floating point value between 0 and 100.
* type: number
*
*/
get rewardPercentile() {
return (params: any[], i: number) => {
const ratio = params[i]
if (typeof ratio !== 'number') {
return {
code: INVALID_PARAMS,
message: `entry at ${i} is not a number`,
}
}
if (ratio < 0) {
return {
code: INVALID_PARAMS,
message: `entry at ${i} is lower than 0`,
}
}
if (ratio > 100) {
return {
code: INVALID_PARAMS,
message: `entry at ${i} is higher than 100`,
}
}
return ratio
}
},

/**
* Verification of rewardPercentiles array
*
* description: A monotonically increasing list of percentile values. For each block in the requested range, the transactions will be sorted in ascending order by effective tip per gas and the coresponding effective tip for the percentile will be determined, accounting for gas consumed.
* type: array
* items: rewardPercentile value
*
*/
get rewardPercentiles() {
return (params: any[], index: number) => {
const field = params[index]
if (!Array.isArray(field)) {
return {
code: INVALID_PARAMS,
message: `invalid argument ${index}: argument is not array`,
}
}
let low = -1
for (let i = 0; i < field.length; i++) {
const ratio = this.rewardPercentile(field, i)
if (typeof ratio === 'object') {
return {
code: INVALID_PARAMS,
message: `invalid argument ${index}: ${ratio.message}`,
}
}
if (ratio <= low) {
return {
code: INVALID_PARAMS,
message: `invalid argument ${index}: array is not monotonically increasing`,
}
}
low = ratio
}
}
},

/**
* validator to ensure that contains one of the string values
* @param values array of possible values
Expand Down
Loading

0 comments on commit a35bf07

Please sign in to comment.