Skip to content

Commit 504212e

Browse files
holgerd77acolytec3
andauthored
Add eth_getMaxPriorityFeePerGas RPC method (#4092)
* Add initial eth_getMaxPriorityFeePerGas RPC method implementation * Add a first simple test setup * Add basic chain creation framing * Add proper createBlock mocking * Expand on test scenarios * Expand on test cases * Revise parameter name in tests --------- Co-authored-by: acolytec3 <17355484+acolytec3@users.noreply.github.com>
1 parent 86d7b9b commit 504212e

File tree

2 files changed

+214
-0
lines changed

2 files changed

+214
-0
lines changed

packages/client/src/rpc/modules/eth.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,11 @@ export class Eth {
330330

331331
this.chainId = callWithStackTrace(this.chainId.bind(this), this._rpcDebug)
332332

333+
this.getMaxPriorityFeePerGas = callWithStackTrace(
334+
this.getMaxPriorityFeePerGas.bind(this),
335+
this._rpcDebug,
336+
)
337+
333338
this.estimateGas = middleware(
334339
callWithStackTrace(this.estimateGas.bind(this), this._rpcDebug),
335340
1,
@@ -541,6 +546,70 @@ export class Eth {
541546
return bigIntToHex(chainId)
542547
}
543548

549+
/**
550+
* Returns an estimate for max priority fee per gas for a tx to be included in a block.
551+
* @returns The max priority fee per gas.
552+
*/
553+
async getMaxPriorityFeePerGas() {
554+
if (!this.client.config.synchronized) {
555+
throw {
556+
code: INTERNAL_ERROR,
557+
message: `client is not aware of the current chain height yet (give sync some more time)`,
558+
}
559+
}
560+
const latest = await this._chain.getCanonicalHeadBlock()
561+
// This ends up with a forward-sorted list of blocks
562+
const blocks = (await this._chain.getBlocks(latest.hash(), 10, 0, true)).reverse()
563+
564+
// Store per-block medians
565+
const blockMedians: bigint[] = []
566+
567+
for (const block of blocks) {
568+
// Array to collect all maxPriorityFeePerGas values
569+
const priorityFees: bigint[] = []
570+
571+
for (const tx of block.transactions) {
572+
// Only EIP-1559 transactions have maxPriorityFeePerGas
573+
if (tx.supports(Capability.EIP1559FeeMarket) === true) {
574+
priorityFees.push((tx as FeeMarket1559Tx).maxPriorityFeePerGas)
575+
}
576+
}
577+
578+
// Calculate median for this block
579+
if (priorityFees.length > 0) {
580+
priorityFees.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
581+
const mid = Math.floor(priorityFees.length / 2)
582+
let median: bigint
583+
if (priorityFees.length % 2 === 0) {
584+
median = (priorityFees[mid - 1] + priorityFees[mid]) / BigInt(2)
585+
} else {
586+
median = priorityFees[mid]
587+
}
588+
blockMedians.push(median)
589+
}
590+
}
591+
592+
// Linear regression to extrapolate next median value
593+
function linearRegression(y: bigint[]): number {
594+
const n = y.length
595+
if (n === 0) return 0
596+
const x = Array.from({ length: n }, (_, i) => i)
597+
const meanX = x.reduce((a, b) => a + b, 0) / n
598+
const meanY = y.reduce((a, b) => a + Number(b), 0) / n
599+
let num = 0,
600+
den = 0
601+
for (let i = 0; i < n; i++) {
602+
num += (x[i] - meanX) * (Number(y[i]) - meanY)
603+
den += (x[i] - meanX) ** 2
604+
}
605+
const a = den === 0 ? 0 : num / den
606+
const b = meanY - a * meanX
607+
return a * n + b // predict next (n-th) value
608+
}
609+
const nextMedianRegression = BigInt(Math.round(linearRegression(blockMedians)))
610+
return bigIntToHex(nextMedianRegression)
611+
}
612+
544613
/**
545614
* Generates and returns an estimate of how much gas is necessary to allow the transaction to complete.
546615
* The transaction will not be added to the blockchain.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { assert, describe, it } from 'vitest'
2+
3+
import { createFeeMarket1559Tx, createLegacyTx } from '@ethereumjs/tx'
4+
import type { TypedTransaction } from '@ethereumjs/tx'
5+
import { hexToBigInt } from '@ethereumjs/util'
6+
import { createClient, createManager, getRPCClient, startRPC } from '../helpers.ts'
7+
8+
const method = 'eth_getMaxPriorityFeePerGas'
9+
10+
type BlocksMPF = bigint[][]
11+
12+
interface Block {
13+
transactions: TypedTransaction[]
14+
hash: () => Uint8Array
15+
}
16+
/**
17+
* @param blockMPF - Representing txs with the respective maxPriorityFeePerGas values
18+
* @param non1559Txs - Number of non-1559 txs to add to the block
19+
* @returns A block representation with the transactions and a hash
20+
*/
21+
function createBlock(blockMPF: bigint[], non1559Txs = 0): Block {
22+
const transactions: TypedTransaction[] = []
23+
for (const mpf of blockMPF) {
24+
const tx = createFeeMarket1559Tx({
25+
maxFeePerGas: mpf, // Side addition to satisfy tx creation
26+
maxPriorityFeePerGas: mpf,
27+
})
28+
transactions.push(tx)
29+
}
30+
for (let i = 0; i < non1559Txs; i++) {
31+
const tx = createLegacyTx({})
32+
transactions.push(tx)
33+
}
34+
35+
return {
36+
transactions,
37+
hash: () => new Uint8Array([1]),
38+
}
39+
}
40+
41+
function createChain(blocksMPF: BlocksMPF = [[]], non1559Txs = 0) {
42+
if (blocksMPF.length === 0) {
43+
throw new Error('call with minimum 1 block')
44+
}
45+
46+
const blocks: Block[] = []
47+
48+
for (const blockMPF of blocksMPF) {
49+
const block = createBlock(blockMPF, non1559Txs)
50+
blocks.push(block)
51+
}
52+
const latest = blocks[blocks.length - 1]
53+
return {
54+
getCanonicalHeadBlock: () => latest,
55+
getBlocks: () => blocks.reverse(), // needs to be returned in reverse order to simulate reverse flag
56+
}
57+
}
58+
59+
async function getSetup(blocksMPF: BlocksMPF, non1559Txs = 0) {
60+
const client = await createClient({ chain: createChain(blocksMPF, non1559Txs) })
61+
client.config.synchronized = true
62+
const manager = createManager(client)
63+
const rpcServer = startRPC(manager.getMethods())
64+
const rpc = getRPCClient(rpcServer)
65+
return { client, manager, rpcServer, rpc }
66+
}
67+
68+
describe(method, () => {
69+
it('should return 0 for a simple block with no transactions', async () => {
70+
const blocksMPF = [[]]
71+
const { rpc } = await getSetup(blocksMPF)
72+
const res = await rpc.request(method, [])
73+
assert.strictEqual(hexToBigInt(res.result), 0n)
74+
})
75+
76+
it('should return "100" for a simple block with one 1559 tx', async () => {
77+
const blocksMPF = [[100n]]
78+
const { rpc } = await getSetup(blocksMPF)
79+
const res = await rpc.request(method, [])
80+
assert.strictEqual(hexToBigInt(res.result), 100n)
81+
})
82+
83+
it('should return 0 for a simple block with one non-1559 tx', async () => {
84+
const blocksMPF = [[]]
85+
const { rpc } = await getSetup(blocksMPF, 1)
86+
const res = await rpc.request(method, [])
87+
assert.strictEqual(hexToBigInt(res.result), 0n)
88+
})
89+
90+
it('should return "100" for two simple blocks with one 1559 tx', async () => {
91+
const blocksMPF = [[100n], [100n]]
92+
const { rpc } = await getSetup(blocksMPF)
93+
const res = await rpc.request(method, [])
94+
assert.strictEqual(hexToBigInt(res.result), 100n)
95+
})
96+
97+
it('should return median for two transactions', async () => {
98+
const blocksMPF = [[100n, 200n]]
99+
const { rpc } = await getSetup(blocksMPF)
100+
const res = await rpc.request(method, [])
101+
assert.strictEqual(hexToBigInt(res.result), 150n)
102+
})
103+
104+
it('should return median for three transactions', async () => {
105+
const blocksMPF = [[100n, 200n, 300n]]
106+
const { rpc } = await getSetup(blocksMPF)
107+
const res = await rpc.request(method, [])
108+
assert.strictEqual(hexToBigInt(res.result), 200n)
109+
})
110+
111+
it('should apply linear regression - clear trend', async () => {
112+
const blocksMPF = [[100n], [200n]]
113+
const { rpc } = await getSetup(blocksMPF)
114+
const res = await rpc.request(method, [])
115+
assert.strictEqual(hexToBigInt(res.result), 300n)
116+
})
117+
118+
it('should apply linear regression - mixed upwards trend (simple)', async () => {
119+
const blocksMPF = [[100n], [200n], [150n]]
120+
const { rpc } = await getSetup(blocksMPF)
121+
const res = await rpc.request(method, [])
122+
assert.strictEqual(hexToBigInt(res.result), 200n)
123+
})
124+
125+
it('should apply linear regression - mixed upwards trend', async () => {
126+
const blocksMPF = [[300n], [200n], [250n], [400n], [700n], [200n]]
127+
const { rpc } = await getSetup(blocksMPF)
128+
const res = await rpc.request(method, [])
129+
assert.strictEqual(hexToBigInt(res.result), 457n)
130+
})
131+
132+
it('should apply median + linear regression in more complex scenarios', async () => {
133+
const blocksMPF = [
134+
[300n, 10n],
135+
[200n, 20n],
136+
[250n, 30n],
137+
[400n, 40n, 100n],
138+
[700n],
139+
[200n, 10n],
140+
]
141+
const { rpc } = await getSetup(blocksMPF)
142+
const res = await rpc.request(method, [])
143+
assert.strictEqual(hexToBigInt(res.result), 366n)
144+
})
145+
})

0 commit comments

Comments
 (0)