From 807830e2ffff4630025ff21965f08989e550ef78 Mon Sep 17 00:00:00 2001 From: tgmichel Date: Thu, 17 Mar 2022 23:15:41 +0100 Subject: [PATCH] Add support for `eth_maxPriorityFeeperGas` (#607) * Add `max_priority_fee_per_gas` rpc method * Remove unused arg * Add ts tests * Simple transfer instead contract create * editorconfig --- client/rpc-core/src/eth.rs | 5 + client/rpc/src/eth.rs | 29 +++++ ts-tests/tests/test-fee-history.ts | 6 +- .../test-max-priority-fee-per-gas-rpc.ts | 109 ++++++++++++++++++ 4 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 ts-tests/tests/test-max-priority-fee-per-gas-rpc.ts diff --git a/client/rpc-core/src/eth.rs b/client/rpc-core/src/eth.rs index 628bc1247d..90fbeea7aa 100644 --- a/client/rpc-core/src/eth.rs +++ b/client/rpc-core/src/eth.rs @@ -188,6 +188,11 @@ pub trait EthApi { newest_block: BlockNumber, reward_percentiles: Option>, ) -> Result; + + /// Introduced in EIP-1159, a Geth-specific and simplified priority fee oracle. + /// Leverages the already existing fee history cache. + #[rpc(name = "eth_maxPriorityFeePerGas")] + fn max_priority_fee_per_gas(&self) -> Result; } /// Eth filters rpc api (polling). diff --git a/client/rpc/src/eth.rs b/client/rpc/src/eth.rs index 4cbaf41b65..156bb5aef7 100644 --- a/client/rpc/src/eth.rs +++ b/client/rpc/src/eth.rs @@ -2427,6 +2427,35 @@ where newest_block ))) } + + fn max_priority_fee_per_gas(&self) -> Result { + // https://github.com/ethereum/go-ethereum/blob/master/eth/ethconfig/config.go#L44-L51 + let at_percentile = 60; + let block_count = 20; + let index = (at_percentile * 2) as usize; + + let highest = + UniqueSaturatedInto::::unique_saturated_into(self.client.info().best_number); + let lowest = highest.saturating_sub(block_count - 1); + + // https://github.com/ethereum/go-ethereum/blob/master/eth/gasprice/gasprice.go#L149 + let mut rewards = Vec::new(); + if let Ok(fee_history_cache) = &self.fee_history_cache.lock() { + for n in lowest..highest + 1 { + if let Some(block) = fee_history_cache.get(&n) { + let reward = if let Some(r) = block.rewards.get(index as usize) { + U256::from(*r) + } else { + U256::zero() + }; + rewards.push(reward); + } + } + } else { + return Err(internal_err(format!("Failed to read fee oracle cache."))); + } + Ok(*rewards.iter().min().unwrap_or(&U256::zero())) + } } pub struct EthFilterApi { diff --git a/ts-tests/tests/test-fee-history.ts b/ts-tests/tests/test-fee-history.ts index ea8b4da0f8..cc1f4d25b4 100644 --- a/ts-tests/tests/test-fee-history.ts +++ b/ts-tests/tests/test-fee-history.ts @@ -34,7 +34,7 @@ describeWithFrontier("Frontier RPC (Fee History)", (context) => { } } - async function createBlocks(block_count, reward_percentiles, priority_fees) { + async function createBlocks(block_count, priority_fees) { for(var b = 0; b < block_count; b++) { for(var p = 0; p < priority_fees.length; p++) { await sendTransaction(context, { @@ -70,7 +70,7 @@ describeWithFrontier("Frontier RPC (Fee History)", (context) => { let block_count = 2; let reward_percentiles = [20,50,70]; let priority_fees = [1, 2, 3]; - await createBlocks(block_count, reward_percentiles, priority_fees); + await createBlocks(block_count, priority_fees); let result = (await customRequest(context.web3, "eth_feeHistory", ["0x2", "latest", reward_percentiles])).result; // baseFeePerGas is always the requested block range + 1 (the next derived base fee). @@ -90,7 +90,7 @@ describeWithFrontier("Frontier RPC (Fee History)", (context) => { let block_count = 11; let reward_percentiles = [20,50,70,85,100]; let priority_fees = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - await createBlocks(block_count, reward_percentiles, priority_fees); + await createBlocks(block_count, priority_fees); let result = (await customRequest(context.web3, "eth_feeHistory", ["0xA", "latest", reward_percentiles])).result; // Calculate the percentiles in javascript. diff --git a/ts-tests/tests/test-max-priority-fee-per-gas-rpc.ts b/ts-tests/tests/test-max-priority-fee-per-gas-rpc.ts new file mode 100644 index 0000000000..17cbddfcf0 --- /dev/null +++ b/ts-tests/tests/test-max-priority-fee-per-gas-rpc.ts @@ -0,0 +1,109 @@ +import { ethers } from "ethers"; +import { expect } from "chai"; +import { step } from "mocha-steps"; + +import { createAndFinalizeBlock, describeWithFrontier, customRequest } from "./util"; + +// We use ethers library in this test as apparently web3js's types are not fully EIP-1559 compliant yet. +describeWithFrontier("Frontier RPC (Max Priority Fee Per Gas)", (context) => { + + const GENESIS_ACCOUNT = "0x6be02d1d3665660d22ff9624b7be0551ee1ac91b"; + const GENESIS_ACCOUNT_PRIVATE_KEY = "0x99B3C12287537E38C90A9219D4CB074A89A16E9CDB20BF85728EBD97C343E342"; + + async function sendTransaction(context, payload: any) { + let signer = new ethers.Wallet(GENESIS_ACCOUNT_PRIVATE_KEY, context.ethersjs); + // Ethers internally matches the locally calculated transaction hash against the one returned as a response. + // Test would fail in case of mismatch. + const tx = await signer.sendTransaction(payload); + return tx; + } + + let nonce = 0; + + function get_percentile(percentile, array) { + array.sort(function (a, b) { return a - b; }); + let index = ((percentile/100) * (array.length))-1; + if (Math.floor(index) == index) { + return array[index]; + } + else { + return Math.ceil((array[Math.floor(index)] + array[Math.ceil(index)])/2); + } + } + + async function createBlocks(block_count, priority_fees) { + for(var b = 0; b < block_count; b++) { + for(var p = 0; p < priority_fees.length; p++) { + await sendTransaction(context, { + from: GENESIS_ACCOUNT, + to: "0x0000000000000000000000000000000000000000", + data: "0x", + value: "0x00", + maxFeePerGas: "0x3B9ACA00", + maxPriorityFeePerGas: context.web3.utils.numberToHex(priority_fees[p]), + accessList: [], + nonce: nonce, + gasLimit: "0x5208", + chainId: 42 + }); + nonce++; + } + await createAndFinalizeBlock(context.web3); + } + } + + step("should default to zero on genesis", async function () { + let result = await customRequest(context.web3, "eth_maxPriorityFeePerGas", []); + expect(result.result).to.be.eq("0x0"); + }); + + step("should default to zero on empty blocks", async function () { + await createAndFinalizeBlock(context.web3); + let result = await customRequest(context.web3, "eth_maxPriorityFeePerGas", []); + expect(result.result).to.be.eq("0x0"); + }); + + // - Create 20 blocks, each with 10 txns. + // - Every txn includes a monotonically increasing tip. + // - The oracle returns the minimum fee in the percentile 60 for the last 20 blocks. + // - In this case, and being the first tip 0, that minimum fee is 5. + step("maxPriorityFeePerGas should suggest the percentile 60 tip", async function () { + this.timeout(100000); + + let block_count = 20; + let txns_per_block = 10; + + let priority_fee = 0; + + for(let i = 0; i < block_count; i++) { + let priority_fees = []; + for(let j = 0; j < txns_per_block; j++) { + priority_fees.push(priority_fee) + priority_fee++; + } + await createBlocks(1, priority_fees); + } + + let result = (await customRequest(context.web3, "eth_maxPriorityFeePerGas", [])).result; + expect(result).to.be.eq("0x5"); + }); + + // If in the last 20 blocks at least one is empty (or only contains zero-tip txns), the + // suggested tip will be zero. + // That's the expected behaviour in this simplified oracle version: there is a decent chance of + // being able to include a zero-tip txn in a low congested network. + step("maxPriorityFeePerGas should suggest zero if there are recent empty blocks", async function () { + this.timeout(100000); + + for(let i = 0; i < 10; i++) { + await createBlocks(1, [0, 1, 2, 3, 4, 5]); + } + await createAndFinalizeBlock(context.web3); + for(let i = 0; i < 9; i++) { + await createBlocks(1, [0, 1, 2, 3, 4, 5]); + } + + let result = (await customRequest(context.web3, "eth_maxPriorityFeePerGas", [])).result; + expect(result).to.be.eq("0x0"); + }); +});