diff --git a/packages/block/src/header.ts b/packages/block/src/header.ts index af3537166d..9d25f2a9da 100644 --- a/packages/block/src/header.ts +++ b/packages/block/src/header.ts @@ -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') ) } @@ -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. */ diff --git a/packages/block/test/eip4844block.spec.ts b/packages/block/test/eip4844block.spec.ts index c2b86400f8..74f87a20ae 100644 --- a/packages/block/test/eip4844block.spec.ts +++ b/packages/block/test/eip4844block.spec.ts @@ -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 }) }) diff --git a/packages/client/src/rpc/modules/eth.ts b/packages/client/src/rpc/modules/eth.ts index 76fd470433..62dd0603bb 100644 --- a/packages/client/src/rpc/modules/eth.ts +++ b/packages/client/src/rpc/modules/eth.ts @@ -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, @@ -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 @@ -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], + ] + ) } /** @@ -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)), + } + } } diff --git a/packages/client/src/rpc/validation.ts b/packages/client/src/rpc/validation.ts index 7fd1755f3c..b10df9618a 100644 --- a/packages/client/src/rpc/validation.ts +++ b/packages/client/src/rpc/validation.ts @@ -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 @@ -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 diff --git a/packages/client/test/rpc/eth/getFeeHistory.spec.ts b/packages/client/test/rpc/eth/getFeeHistory.spec.ts new file mode 100644 index 0000000000..68be2c6b81 --- /dev/null +++ b/packages/client/test/rpc/eth/getFeeHistory.spec.ts @@ -0,0 +1,449 @@ +import { Common, Chain as CommonChain, Hardfork } from '@ethereumjs/common' +import { TransactionFactory } from '@ethereumjs/tx' +import { + Address, + BIGINT_0, + BIGINT_256, + bigIntToHex, + blobsToCommitments, + bytesToBigInt, + commitmentsToVersionedHashes, + getBlobs, + initKZG, +} from '@ethereumjs/util' +import { hexToBytes } from 'ethereum-cryptography/utils' +import { createKZG } from 'kzg-wasm' +import { assert, describe, it } from 'vitest' + +import genesisJSON from '../../testdata/geth-genesis/eip4844.json' +import pow from '../../testdata/geth-genesis/pow.json' +import { getRpcClient, gethGenesisStartLondon, setupChain } from '../helpers' + +import type { Chain } from '../../../src/blockchain' +import type { VMExecution } from '../../../src/execution' + +const method = 'eth_feeHistory' + +const privateKey = hexToBytes('0xe331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109') +const pKeyAddress = Address.fromPrivateKey(privateKey) + +const privateKey4844 = hexToBytes( + '0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8' +) +const p4844Address = Address.fromPrivateKey(privateKey4844) + +const produceFakeGasUsedBlock = async (execution: VMExecution, chain: Chain, gasUsed: bigint) => { + const { vm } = execution + const parentBlock = await chain.getCanonicalHeadBlock() + const vmCopy = await vm.shallowCopy() + // Set block's gas used to max + const blockBuilder = await vmCopy.buildBlock({ + parentBlock, + headerData: { + timestamp: parentBlock.header.timestamp + BigInt(1), + gasUsed: bigIntToHex(gasUsed), + }, + blockOpts: { + calcDifficultyFromHeader: parentBlock.header, + putBlockIntoBlockchain: false, + }, + }) + blockBuilder.gasUsed = gasUsed + + const block = await blockBuilder.build() + await chain.putBlocks([block], false) + //await execution.run() +} + +/** + * This method builds a block on top of the current head block + * It allows two optional parameters which will determine the gas limit + maxPriorityFeesPerGas + * The txs will all completely revert, so all gas limit of the tx will be consumed + * @param execution + * @param chain + * @param maxPriorityFeesPerGas Optional array of the maxPriorityFeesPerGas per tx + * @param gasLimits Optional array of the gasLimit per tx. This array length should be equal to maxPriorityFeesPerGas length + */ +const produceBlockWithTx = async ( + execution: VMExecution, + chain: Chain, + maxPriorityFeesPerGas: bigint[] = [BigInt(0xff)], + gasLimits: bigint[] = [BigInt(0xfffff)] +) => { + const { vm } = execution + const account = await vm.stateManager.getAccount(pKeyAddress) + let nonce = account?.nonce ?? BIGINT_0 + const parentBlock = await chain.getCanonicalHeadBlock() + const vmCopy = await vm.shallowCopy() + // Set block's gas used to max + const blockBuilder = await vmCopy.buildBlock({ + parentBlock, + headerData: { + timestamp: parentBlock.header.timestamp + BigInt(1), + }, + blockOpts: { + calcDifficultyFromHeader: parentBlock.header, + putBlockIntoBlockchain: false, + }, + }) + for (let i = 0; i < maxPriorityFeesPerGas.length; i++) { + const maxPriorityFeePerGas = maxPriorityFeesPerGas[i] + const gasLimit = gasLimits[i] + await blockBuilder.addTransaction( + TransactionFactory.fromTxData( + { + type: 2, + gasLimit, + maxFeePerGas: 0xffffffff, + maxPriorityFeePerGas, + nonce, + data: '0xFE', + }, + { common: vmCopy.common } + ).sign(privateKey) + ) + nonce++ + } + + const block = await blockBuilder.build() + await chain.putBlocks([block], false) + await execution.run() +} + +/** + * This method builds a block on top of the current head block and will insert 4844 txs + * @param execution + * @param chain + * @param blobsCount Array of blob txs to produce. The amount of blobs in here is thus the amount of blobs per tx. + */ +const produceBlockWith4844Tx = async ( + execution: VMExecution, + chain: Chain, + blobsCount: number[] +) => { + const kzg = await createKZG() + initKZG(kzg) + // 4844 sample blob + const sampleBlob = getBlobs('hello world') + const commitment = blobsToCommitments(sampleBlob) + const blobVersionedHash = commitmentsToVersionedHashes(commitment) + + const { vm } = execution + const account = await vm.stateManager.getAccount(p4844Address) + let nonce = account?.nonce ?? BIGINT_0 + const parentBlock = await chain.getCanonicalHeadBlock() + const vmCopy = await vm.shallowCopy() + // Set block's gas used to max + const blockBuilder = await vmCopy.buildBlock({ + parentBlock, + headerData: { + timestamp: parentBlock.header.timestamp + BigInt(1), + }, + blockOpts: { + calcDifficultyFromHeader: parentBlock.header, + putBlockIntoBlockchain: false, + }, + }) + for (let i = 0; i < blobsCount.length; i++) { + const blobVersionedHashes = [] + const blobs = [] + const kzgCommitments = [] + const to = Address.zero() + if (blobsCount[i] > 0) { + for (let blob = 0; blob < blobsCount[i]; blob++) { + blobVersionedHashes.push(...blobVersionedHash) + blobs.push(...sampleBlob) + kzgCommitments.push(...commitment) + } + } + await blockBuilder.addTransaction( + TransactionFactory.fromTxData( + { + type: 3, + gasLimit: 21000, + maxFeePerGas: 0xffffffff, + maxPriorityFeePerGas: BIGINT_256, + nonce, + to, + blobVersionedHashes, + blobs, + kzgCommitments, + maxFeePerBlobGas: BigInt(1000), + }, + { common: vmCopy.common } + ).sign(privateKey4844) + ) + nonce++ + } + + const block = await blockBuilder.build() + await chain.putBlocks([block], true) + await execution.run() +} + +describe(method, () => { + it(`${method}: should return 12.5% increased baseFee if parent block is full`, async () => { + const { chain, server, execution } = await setupChain(gethGenesisStartLondon(pow), 'powLondon') + const gasUsed = bytesToBigInt(hexToBytes(pow.gasLimit)) + + // Produce 3 fake blocks on the chain. + // This also ensures that the correct blocks are being retrieved. + await produceFakeGasUsedBlock(execution, chain, gasUsed / BigInt(2)) + await produceFakeGasUsedBlock(execution, chain, gasUsed / BigInt(2)) + await produceFakeGasUsedBlock(execution, chain, gasUsed) + + const rpc = getRpcClient(server) + + // Expect to retrieve the blocks [2,3] + const res = await rpc.request(method, ['0x2', 'latest', []]) + const [firstBaseFee, previousBaseFee, nextBaseFee] = res.result.baseFeePerGas as [ + string, + string, + string + ] + const increase = + Number( + (1000n * + (bytesToBigInt(hexToBytes(nextBaseFee)) - bytesToBigInt(hexToBytes(previousBaseFee)))) / + bytesToBigInt(hexToBytes(previousBaseFee)) + ) / 1000 + + // Note: this also ensures that block 2,3 are returned, since gas of block 0 -> 1 and 1 -> 2 does not change + assert.equal(increase, 0.125) + // Sanity check + assert.equal(firstBaseFee, previousBaseFee) + // 2 blocks are requested, but the next baseFee is able to be calculated from the latest block + // Per spec, also return this. So return 3 baseFeePerGas + assert.equal(res.result.baseFeePerGas.length, 3) + + // Check that the expected gasRatios of the blocks are correct + assert.equal(res.result.gasUsedRatio[0], 0.5) // Block 2 + assert.equal(res.result.gasUsedRatio[1], 1) // Block 3 + + // No ratios were requested + assert.deepEqual(res.result.reward, [[], []]) + + // oldestBlock is correct + assert.equal(res.result.oldestBlock, '0x2') + }) + + it(`${method}: should return 12.5% decreased base fee if the block is empty`, async () => { + const { chain, server, execution } = await setupChain(gethGenesisStartLondon(pow), 'powLondon') + const gasUsed = BigInt(0) + + await produceFakeGasUsedBlock(execution, chain, gasUsed) + const rpc = getRpcClient(server) + + const res = await rpc.request(method, ['0x2', 'latest', []]) + const [, previousBaseFee, nextBaseFee] = res.result.baseFeePerGas as [string, string, string] + const decrease = + Number( + (1000n * + (bytesToBigInt(hexToBytes(nextBaseFee)) - bytesToBigInt(hexToBytes(previousBaseFee)))) / + bytesToBigInt(hexToBytes(previousBaseFee)) + ) / 1000 + + assert.equal(decrease, -0.125) + }) + + it(`${method}: should return initial base fee if the block number is london hard fork`, async () => { + const common = new Common({ + eips: [1559], + chain: CommonChain.Mainnet, + hardfork: Hardfork.London, + }) + + const initialBaseFee = common.param('gasConfig', 'initialBaseFee') + const { server } = await setupChain(gethGenesisStartLondon(pow), 'powLondon') + + const rpc = getRpcClient(server) + + const res = await rpc.request(method, ['0x1', 'latest', []]) + + const [baseFee] = res.result.baseFeePerGas as [string] + + assert.equal(bytesToBigInt(hexToBytes(baseFee)), initialBaseFee) + }) + + it(`${method}: should return 0x0 for base fees requested before eip-1559`, async () => { + const { chain, execution, server } = await setupChain(pow, 'pow') + const gasUsed = BigInt(0) + + await produceFakeGasUsedBlock(execution, chain, gasUsed) + + const rpc = getRpcClient(server) + + const res = await rpc.request(method, ['0x1', 'latest', []]) + + const [previousBaseFee, nextBaseFee] = res.result.baseFeePerGas as [string, string] + + assert.equal(previousBaseFee, '0x0') + assert.equal(nextBaseFee, '0x0') + }) + + it(`${method}: should return correct gas used ratios`, async () => { + const { chain, server, execution } = await setupChain(gethGenesisStartLondon(pow), 'powLondon') + const gasUsed = bytesToBigInt(hexToBytes(pow.gasLimit)) / 2n + + await produceFakeGasUsedBlock(execution, chain, gasUsed) + + const rpc = getRpcClient(server) + + const res = await rpc.request(method, ['0x2', 'latest', []]) + + const [genesisGasUsedRatio, nextGasUsedRatio] = res.result.gasUsedRatio as [number, number] + + assert.equal(genesisGasUsedRatio, 0) + assert.equal(nextGasUsedRatio, 0.5) + }) + + it(`${method}: should throw error if block count is below 1`, async () => { + const { server } = await setupChain(gethGenesisStartLondon(pow), 'powLondon') + + const rpc = getRpcClient(server) + + const req = await rpc.request(method, ['0x0', 'latest', []]) + assert.ok(req.error !== undefined) + }) + + it(`${method}: should throw error if block count is above 1024`, async () => { + const { server } = await setupChain(gethGenesisStartLondon(pow), 'powLondon') + + const rpc = getRpcClient(server) + + const req = await rpc.request(method, ['0x401', 'latest', []]) + assert.ok(req.error !== undefined) + }) + + it(`${method}: should generate reward percentiles with 0s`, async () => { + const { chain, server, execution } = await setupChain(gethGenesisStartLondon(pow), 'powLondon') + await produceFakeGasUsedBlock(execution, chain, 1n) + + const rpc = getRpcClient(server) + const res = await rpc.request(method, ['0x1', 'latest', [50, 60]]) + assert.equal( + parseInt(res.result.reward[0][0]), + 0, + 'Should return 0 for empty block reward percentiles' + ) + assert.equal( + res.result.reward[0][1], + '0x0', + 'Should return 0 for empty block reward percentiles' + ) + }) + it(`${method}: should generate reward percentiles`, async () => { + const { chain, server, execution } = await setupChain(gethGenesisStartLondon(pow), 'powLondon') + await produceBlockWithTx(execution, chain) + + const rpc = getRpcClient(server) + const res = await rpc.request(method, ['0x1', 'latest', [50]]) + assert.ok(res.result.reward[0].length > 0, 'Produced at least one rewards percentile') + }) + + it(`${method}: should generate reward percentiles`, async () => { + const { chain, server, execution } = await setupChain(gethGenesisStartLondon(pow), 'powLondon') + await produceBlockWithTx(execution, chain) + + const rpc = getRpcClient(server) + const res = await rpc.request(method, ['0x1', 'latest', [50]]) + assert.ok(res.result.reward[0].length > 0, 'Produced at least one rewards percentile') + }) + + it(`${method}: should generate reward percentiles - sorted check`, async () => { + const { chain, server, execution } = await setupChain(gethGenesisStartLondon(pow), 'powLondon') + const priorityFees = [BigInt(100), BigInt(200)] + const gasUsed = [BigInt(400000), BigInt(600000)] + await produceBlockWithTx(execution, chain, priorityFees, gasUsed) + + const rpc = getRpcClient(server) + const res = await rpc.request(method, ['0x1', 'latest', [40, 100]]) + assert.ok(res.result.reward[0].length > 0, 'Produced at least one rewards percentile') + const expected = priorityFees.map(bigIntToHex) + assert.deepEqual(res.result.reward[0], expected) + + // If the txs order is swapped, the output should still be the same + // This tests that the txs are ordered in ascending order of `priorityFee` + await produceBlockWithTx(execution, chain, priorityFees.reverse(), gasUsed.reverse()) + + const res2 = await rpc.request(method, ['0x1', 'latest', [40, 100]]) + assert.ok(res.result.reward[0].length > 0, 'Produced at least one rewards percentile') + assert.deepEqual(res2.result.reward[0], expected) + }) + + it(`${method} - reward percentiles - should return the correct reward percentiles`, async () => { + const { chain, server, execution } = await setupChain(gethGenesisStartLondon(pow), 'powLondon') + const priorityFees = [BigInt(100), BigInt(200)] + const gasUsed = [BigInt(500000), BigInt(500000)] + await produceBlockWithTx(execution, chain, priorityFees, gasUsed) + + const rpc = getRpcClient(server) + /** + * In this test, both txs use 50% of the block gas used + * Request the reward percentiles [10, 20, 60, 100] so expect rewards of: + * [tx1, tx1, tx2, tx2] + */ + const res = await rpc.request(method, ['0x1', 'latest', [10, 20, 60, 100]]) + + const expected = [priorityFees[0], priorityFees[0], priorityFees[1], priorityFees[1]].map( + bigIntToHex + ) + assert.deepEqual(res.result.reward[0], expected) + + // Check that pre-4844 blocks have 0-filled arrays + assert.deepEqual(res.result.baseFeePerBlobGas, ['0x0', '0x0']) + assert.deepEqual(res.result.blobGasUsedRatio, [0]) + }) + + /** + * 4844-related test + */ + it( + `${method} - Should correctly return the right blob base fees and ratios for a chain with 4844 active`, + async () => { + const kzg = await createKZG() + initKZG(kzg) + const { chain, execution, server } = await setupChain(genesisJSON, 'post-merge', { + engine: true, + hardfork: Hardfork.Cancun, + customCrypto: { + kzg, + }, + }) + + // Start cranking up the initial blob gas for some more "realistic" testing + + for (let i = 0; i < 10; i++) { + await produceBlockWith4844Tx(execution, chain, [6]) + } + + // Now for the actual test: create 6 blocks each with a decreasing amount of blobs + for (let i = 6; i > 0; i--) { + await produceBlockWith4844Tx(execution, chain, [i]) + } + + const rpc = getRpcClient(server) + + const res = await rpc.request(method, ['0x6', 'latest', []]) + + const head = await chain.getCanonicalHeadBlock() + + const expBlobGas = [] + const expRatio = [1, 5 / 6, 4 / 6, 3 / 6, 2 / 6, 1 / 6] + + for (let i = 5; i >= 0; i--) { + const blockNumber = head.header.number - BigInt(i) + const block = await chain.getBlock(blockNumber) + expBlobGas.push(bigIntToHex(block.header.getBlobGasPrice())) + } + + expBlobGas.push(bigIntToHex(head.header.calcNextBlobGasPrice())) + + assert.deepEqual(res.result.baseFeePerBlobGas, expBlobGas) + assert.deepEqual(res.result.blobGasUsedRatio, expRatio) + }, + { + timeout: 60000, + } + ) +}) diff --git a/packages/client/test/rpc/eth/getTransactionByBlockHashAndIndex.spec.ts b/packages/client/test/rpc/eth/getTransactionByBlockHashAndIndex.spec.ts index 6d80d17ccf..47a4c00884 100644 --- a/packages/client/test/rpc/eth/getTransactionByBlockHashAndIndex.spec.ts +++ b/packages/client/test/rpc/eth/getTransactionByBlockHashAndIndex.spec.ts @@ -35,7 +35,7 @@ describe(method, async () => { it('call with valid arguments', async () => { const { rpc } = await setUp() - const mockBlockHash = '0x572856aae9a653012a7df7aeb56bfb7fe77f5bcb4b69fd971c04e989f6ccf9b1' + const mockBlockHash = '0x0d52ca94a881e32dfe40db79623745b29883f4fe2ae23c14d01889bce4c069c0' const mockTxHash = '0x13548b649129ad9beb57467a819d24b846fa0aa02a955f6e974541e1ebb8b02c' const mockTxIndex = '0x1' diff --git a/packages/client/test/rpc/validation.spec.ts b/packages/client/test/rpc/validation.spec.ts index ce72639cd1..69968df0d1 100644 --- a/packages/client/test/rpc/validation.spec.ts +++ b/packages/client/test/rpc/validation.spec.ts @@ -592,6 +592,61 @@ describe(prefix, () => { assert.notOk(validatorResult(validators.array(validators.bool)([[true, 'true']], 0))) }) + it('rewardPercentile', () => { + // valid + assert.equal(validators.rewardPercentile([0], 0), 0) + assert.equal(validators.rewardPercentile([0.1], 0), 0.1) + assert.equal(validators.rewardPercentile([10], 0), 10) + assert.equal(validators.rewardPercentile([100], 0), 100) + + // invalid + assert.deepEqual(validators.rewardPercentile([-1], 0), { + code: INVALID_PARAMS, + message: `entry at 0 is lower than 0`, + }) + assert.deepEqual(validators.rewardPercentile([101], 0), { + code: INVALID_PARAMS, + message: `entry at 0 is higher than 100`, + }) + assert.deepEqual(validators.rewardPercentile([], 0), { + code: INVALID_PARAMS, + message: `entry at 0 is not a number`, + }) + assert.deepEqual(validators.rewardPercentile(['0'], 0), { + code: INVALID_PARAMS, + message: `entry at 0 is not a number`, + }) + }) + + it('rewardPercentiles', () => { + // valid + assert.ok(validatorResult(validators.rewardPercentiles([[]], 0))) + assert.ok(validatorResult(validators.rewardPercentiles([[0]], 0))) + assert.ok(validatorResult(validators.rewardPercentiles([[100]], 0))) + assert.ok(validatorResult(validators.rewardPercentiles([[0, 2, 5, 30, 100]], 0))) + assert.ok(validatorResult(validators.rewardPercentiles([[0, 2.1, 5.35, 30.999, 60, 100]], 0))) + + // invalid + assert.notOk(validatorResult(validators.rewardPercentiles([[[]]], 0))) // Argument is not number + assert.notOk(validatorResult(validators.rewardPercentiles([[-1]], 0))) // Argument < 0 + assert.notOk(validatorResult(validators.rewardPercentiles([[100.1]], 0))) // Argument > 100 + assert.notOk(validatorResult(validators.rewardPercentiles([[1, 2, 3, 2.5]], 0))) // Not monotonically increasing + assert.notOk(validatorResult(validators.rewardPercentiles([0], 0))) // Input not array + }) + + it('integer', () => { + //valid + assert.ok(validatorResult(validators.integer([1], 0))) + assert.ok(validatorResult(validators.integer([-1], 0))) + assert.ok(validatorResult(validators.integer([0], 0))) + + //invalid + assert.notOk(validatorResult(validators.integer(['a'], 0))) + assert.notOk(validatorResult(validators.integer([1.234], 0))) + assert.notOk(validatorResult(validators.integer([undefined], 0))) + assert.notOk(validatorResult(validators.integer([null], 0))) + }) + it('values', () => { // valid assert.ok(validatorResult(validators.values(['VALID', 'INVALID'])(['VALID'], 0))) diff --git a/packages/client/test/testdata/geth-genesis/pow.json b/packages/client/test/testdata/geth-genesis/pow.json index d3f36e689f..58b85c0659 100644 --- a/packages/client/test/testdata/geth-genesis/pow.json +++ b/packages/client/test/testdata/geth-genesis/pow.json @@ -791,6 +791,9 @@ }, "cde098d93535445768e8a2345a2f869139f45641": { "balance": "0x200000000000000000000000000000000000000000000000000000000000000" + }, + "be862ad9abfe6f22bcb087716c7d89a26051f74c": { + "balance": "0x200000000000000000000000000000000000000000000000000000000000000" } }, "number": "0x0", diff --git a/packages/tx/src/baseTransaction.ts b/packages/tx/src/baseTransaction.ts index 813c6b738d..fd56e67609 100644 --- a/packages/tx/src/baseTransaction.ts +++ b/packages/tx/src/baseTransaction.ts @@ -205,6 +205,13 @@ export abstract class BaseTransaction return cost } + /** + * Returns the effective priority fee. This is the priority fee which the coinbase will receive + * once it is included in the block + * @param baseFee Optional baseFee of the block. Note for EIP1559 and EIP4844 this is required. + */ + abstract getEffectivePriorityFee(baseFee: bigint | undefined): bigint + /** * The up front amount that an account must have for this transaction to be valid */ diff --git a/packages/tx/src/capabilities/eip1559.ts b/packages/tx/src/capabilities/eip1559.ts index d10375c9af..d76c744f39 100644 --- a/packages/tx/src/capabilities/eip1559.ts +++ b/packages/tx/src/capabilities/eip1559.ts @@ -7,3 +7,17 @@ export function getUpfrontCost(tx: EIP1559CompatibleTx, baseFee: bigint): bigint const gasPrice = inclusionFeePerGas + baseFee return tx.gasLimit * gasPrice + tx.value } + +export function getEffectivePriorityFee( + tx: EIP1559CompatibleTx, + baseFee: bigint | undefined +): bigint { + if (baseFee === undefined || baseFee > tx.maxFeePerGas) { + throw new Error('Tx cannot pay baseFee') + } + + // The remaining fee for the coinbase, which can take up to this value, capped at `maxPriorityFeePerGas` + const remainingFee = tx.maxFeePerGas - baseFee + + return tx.maxPriorityFeePerGas < remainingFee ? tx.maxPriorityFeePerGas : remainingFee +} diff --git a/packages/tx/src/capabilities/legacy.ts b/packages/tx/src/capabilities/legacy.ts index d57f7c5b0e..e2eda09199 100644 --- a/packages/tx/src/capabilities/legacy.ts +++ b/packages/tx/src/capabilities/legacy.ts @@ -101,3 +101,15 @@ export function getSenderPublicKey(tx: LegacyTxInterface): Uint8Array { throw new Error(msg) } } + +export function getEffectivePriorityFee(gasPrice: bigint, baseFee: bigint | undefined): bigint { + if (baseFee !== undefined && baseFee > gasPrice) { + throw new Error('Tx cannot pay baseFee') + } + + if (baseFee === undefined) { + return gasPrice + } + + return gasPrice - baseFee +} diff --git a/packages/tx/src/eip1559Transaction.ts b/packages/tx/src/eip1559Transaction.ts index 5223f38937..f0332f7849 100644 --- a/packages/tx/src/eip1559Transaction.ts +++ b/packages/tx/src/eip1559Transaction.ts @@ -206,6 +206,14 @@ export class FeeMarketEIP1559Transaction extends BaseTransaction { } } + getEffectivePriorityFee(baseFee?: bigint): bigint { + return Legacy.getEffectivePriorityFee(this.gasPrice, baseFee) + } + /** * Returns a Uint8Array Array of the raw Bytes of the legacy transaction, in order. * diff --git a/packages/tx/test/eip1559.spec.ts b/packages/tx/test/eip1559.spec.ts index ccee007e5c..05e3b11168 100644 --- a/packages/tx/test/eip1559.spec.ts +++ b/packages/tx/test/eip1559.spec.ts @@ -85,6 +85,23 @@ describe('[FeeMarketEIP1559Transaction]', () => { ) }) + it('getEffectivePriorityFee()', () => { + const tx = FeeMarketEIP1559Transaction.fromTxData( + { + maxFeePerGas: 10, + maxPriorityFeePerGas: 8, + }, + { common } + ) + assert.equal(tx.getEffectivePriorityFee(BigInt(10)), BigInt(0)) + assert.equal(tx.getEffectivePriorityFee(BigInt(9)), BigInt(1)) + assert.equal(tx.getEffectivePriorityFee(BigInt(8)), BigInt(2)) + assert.equal(tx.getEffectivePriorityFee(BigInt(2)), BigInt(8)) + assert.equal(tx.getEffectivePriorityFee(BigInt(1)), BigInt(8)) + assert.equal(tx.getEffectivePriorityFee(BigInt(0)), BigInt(8)) + assert.throws(() => tx.getEffectivePriorityFee(BigInt(11))) + }) + it('sign()', () => { for (let index = 0; index < testdata.length; index++) { const data = testdata[index] diff --git a/packages/tx/test/eip4844.spec.ts b/packages/tx/test/eip4844.spec.ts index 0e338d7129..a6a76952f0 100644 --- a/packages/tx/test/eip4844.spec.ts +++ b/packages/tx/test/eip4844.spec.ts @@ -550,6 +550,32 @@ describe('hash() and signature verification', () => { }) }) +it('getEffectivePriorityFee()', async () => { + const kzg = await createKZG() + initKZG(kzg, '') + const common = Common.fromGethGenesis(gethGenesis, { + chain: 'customChain', + hardfork: Hardfork.Cancun, + customCrypto: { kzg }, + }) + const tx = BlobEIP4844Transaction.fromTxData( + { + maxFeePerGas: 10, + maxPriorityFeePerGas: 8, + to: Address.zero(), + blobVersionedHashes: [concatBytes(new Uint8Array([1]), randomBytes(31))], + }, + { common } + ) + assert.equal(tx.getEffectivePriorityFee(BigInt(10)), BigInt(0)) + assert.equal(tx.getEffectivePriorityFee(BigInt(9)), BigInt(1)) + assert.equal(tx.getEffectivePriorityFee(BigInt(8)), BigInt(2)) + assert.equal(tx.getEffectivePriorityFee(BigInt(2)), BigInt(8)) + assert.equal(tx.getEffectivePriorityFee(BigInt(1)), BigInt(8)) + assert.equal(tx.getEffectivePriorityFee(BigInt(0)), BigInt(8)) + assert.throws(() => tx.getEffectivePriorityFee(BigInt(11))) +}) + describe('Network wrapper deserialization test', () => { let common: Common beforeAll(async () => { diff --git a/packages/tx/test/legacy.spec.ts b/packages/tx/test/legacy.spec.ts index 8c1b8220e4..dfd17b6ed8 100644 --- a/packages/tx/test/legacy.spec.ts +++ b/packages/tx/test/legacy.spec.ts @@ -213,6 +213,17 @@ describe('[Transaction]', () => { assert.equal(tx.getDataFee(), BigInt(240)) }) + it('getEffectivePriorityFee() -> should return correct values', () => { + const tx = LegacyTransaction.fromTxData({ + gasPrice: BigInt(100), + }) + + assert.equal(tx.getEffectivePriorityFee(), BigInt(100)) + assert.equal(tx.getEffectivePriorityFee(BigInt(20)), BigInt(80)) + assert.equal(tx.getEffectivePriorityFee(BigInt(100)), BigInt(0)) + assert.throws(() => tx.getEffectivePriorityFee(BigInt(101))) + }) + it('getUpfrontCost() -> should return upfront cost', () => { const tx = LegacyTransaction.fromTxData({ gasPrice: 1000, diff --git a/packages/tx/test/typedTxsAndEIP2930.spec.ts b/packages/tx/test/typedTxsAndEIP2930.spec.ts index b35f8bc505..4747abe21f 100644 --- a/packages/tx/test/typedTxsAndEIP2930.spec.ts +++ b/packages/tx/test/typedTxsAndEIP2930.spec.ts @@ -535,6 +535,17 @@ describe('[AccessListEIP2930Transaction] -> Class Specific Tests', () => { ) }) + it('getEffectivePriorityFee() -> should return correct values', () => { + const tx = AccessListEIP2930Transaction.fromTxData({ + gasPrice: BigInt(100), + }) + + assert.equal(tx.getEffectivePriorityFee(), BigInt(100)) + assert.equal(tx.getEffectivePriorityFee(BigInt(20)), BigInt(80)) + assert.equal(tx.getEffectivePriorityFee(BigInt(100)), BigInt(0)) + assert.throws(() => tx.getEffectivePriorityFee(BigInt(101))) + }) + it('getUpfrontCost() -> should return upfront cost', () => { const tx = AccessListEIP2930Transaction.fromTxData( { diff --git a/packages/util/src/bytes.ts b/packages/util/src/bytes.ts index 3e87eceb52..7100ad3544 100644 --- a/packages/util/src/bytes.ts +++ b/packages/util/src/bytes.ts @@ -402,6 +402,18 @@ export const bigIntToHex = (num: bigint): PrefixedHexString => { return '0x' + num.toString(16) } +/** + * Calculates max bigint from an array of bigints + * @param args array of bigints + */ +export const bigIntMax = (...args: bigint[]) => args.reduce((m, e) => (e > m ? e : m)) + +/** + * Calculates min BigInt from an array of BigInts + * @param args array of bigints + */ +export const bigIntMin = (...args: bigint[]) => args.reduce((m, e) => (e < m ? e : m)) + /** * Convert value from bigint to an unpadded Uint8Array * (useful for RLP transport) diff --git a/packages/vm/src/runTx.ts b/packages/vm/src/runTx.ts index 10816d043c..24aeb8ee51 100644 --- a/packages/vm/src/runTx.ts +++ b/packages/vm/src/runTx.ts @@ -443,12 +443,9 @@ async function _runTx(this: VM, opts: RunTxOpts): Promise { let inclusionFeePerGas: bigint // EIP-1559 tx if (tx.supports(Capability.EIP1559FeeMarket)) { + // TODO make txs use the new getEffectivePriorityFee const baseFee = block.header.baseFeePerGas! - inclusionFeePerGas = - (tx as FeeMarketEIP1559Transaction).maxPriorityFeePerGas < - (tx as FeeMarketEIP1559Transaction).maxFeePerGas - baseFee - ? (tx as FeeMarketEIP1559Transaction).maxPriorityFeePerGas - : (tx as FeeMarketEIP1559Transaction).maxFeePerGas - baseFee + inclusionFeePerGas = tx.getEffectivePriorityFee(baseFee) gasPrice = inclusionFeePerGas + baseFee } else {