feat: gas price oracle (#2600)

This commit is contained in:
Dan Cline
2023-05-12 15:15:34 -04:00
committed by GitHub
parent 059e9c9a29
commit 52b5418a63
12 changed files with 337 additions and 22 deletions

View File

@@ -86,6 +86,9 @@ pub enum ProviderError {
/// Thrown when the cache service task dropped
#[error("cache service task stopped")]
CacheServiceUnavailable,
/// Thrown when the gas oracle task dropped
#[error("gas oracle task stopped")]
GasPriceOracleServiceUnavailable,
/// Thrown when we failed to lookup a block for the pending state
#[error("Unknown block hash: {0:}")]
UnknownBlockHash(H256),

View File

@@ -14,8 +14,8 @@ use reth_provider::{
BlockProviderIdExt, EvmEnvProvider, HeaderProvider, ReceiptProviderIdExt, StateProviderFactory,
};
use reth_rpc::{
eth::cache::EthStateCache, AuthLayer, Claims, EngineEthApi, EthApi, EthFilter,
JwtAuthValidator, JwtSecret,
eth::{cache::EthStateCache, gas_oracle::GasPriceOracle},
AuthLayer, Claims, EngineEthApi, EthApi, EthFilter, JwtAuthValidator, JwtSecret,
};
use reth_rpc_api::{servers::*, EngineApiServer};
use reth_tasks::TaskSpawner;
@@ -51,8 +51,9 @@ where
EngineApi: EngineApiServer,
{
// spawn a new cache task
let eth_cache = EthStateCache::spawn_with(client.clone(), Default::default(), executor);
let eth_api = EthApi::new(client.clone(), pool.clone(), network, eth_cache.clone());
let eth_cache = EthStateCache::spawn_with(client.clone(), Default::default(), executor.clone());
let gas_oracle = GasPriceOracle::new(client.clone(), Default::default(), eth_cache.clone());
let eth_api = EthApi::new(client.clone(), pool.clone(), network, eth_cache.clone(), gas_oracle);
let eth_filter = EthFilter::new(client, pool, eth_cache.clone(), DEFAULT_MAX_LOGS_IN_RESPONSE);
launch_with_eth_api(eth_api, eth_filter, engine_api, socket_addr, secret).await
}

View File

@@ -1,5 +1,8 @@
use reth_rpc::{
eth::cache::{EthStateCache, EthStateCacheConfig},
eth::{
cache::{EthStateCache, EthStateCacheConfig},
gas_oracle::GasPriceOracleConfig,
},
EthApi, EthFilter, EthPubSub,
};
use serde::{Deserialize, Serialize};
@@ -25,6 +28,8 @@ pub struct EthHandlers<Client, Pool, Network, Events> {
pub struct EthConfig {
/// Settings for the caching layer
pub cache: EthStateCacheConfig,
/// Settings for the gas price oracle
pub gas_oracle: GasPriceOracleConfig,
/// The maximum number of tracing calls that can be executed in concurrently.
pub max_tracing_requests: usize,
/// Maximum number of logs that can be returned in a single response in `eth_getLogs` calls.
@@ -35,6 +40,7 @@ impl Default for EthConfig {
fn default() -> Self {
Self {
cache: EthStateCacheConfig::default(),
gas_oracle: GasPriceOracleConfig::default(),
max_tracing_requests: 10,
max_logs_per_response: DEFAULT_MAX_LOGS_IN_RESPONSE,
}

View File

@@ -110,8 +110,9 @@ use reth_provider::{
StateProviderFactory,
};
use reth_rpc::{
eth::cache::EthStateCache, AdminApi, DebugApi, EngineEthApi, EthApi, EthFilter, EthPubSub,
EthSubscriptionIdProvider, NetApi, TraceApi, TracingCallGuard, TxPoolApi, Web3Api,
eth::{cache::EthStateCache, gas_oracle::GasPriceOracle},
AdminApi, DebugApi, EngineEthApi, EthApi, EthFilter, EthPubSub, EthSubscriptionIdProvider,
NetApi, TraceApi, TracingCallGuard, TxPoolApi, Web3Api,
};
use reth_rpc_api::{servers::*, EngineApiServer};
use reth_tasks::TaskSpawner;
@@ -826,6 +827,11 @@ where
self.config.eth.cache.clone(),
self.executor.clone(),
);
let gas_oracle = GasPriceOracle::new(
self.client.clone(),
self.config.eth.gas_oracle.clone(),
cache.clone(),
);
let new_canonical_blocks = self.events.canonical_state_stream();
let c = cache.clone();
self.executor.spawn_critical(
@@ -840,6 +846,7 @@ where
self.pool.clone(),
self.network.clone(),
cache.clone(),
gas_oracle,
);
let filter = EthFilter::new(
self.client.clone(),

View File

@@ -97,8 +97,8 @@ where
EthApiClient::send_transaction(client, transaction_request).await.unwrap_err();
EthApiClient::hashrate(client).await.unwrap();
EthApiClient::submit_hashrate(client, U256::default(), H256::default()).await.unwrap();
EthApiClient::gas_price(client).await.unwrap();
EthApiClient::max_priority_fee_per_gas(client).await.unwrap();
EthApiClient::gas_price(client).await.unwrap_err();
EthApiClient::max_priority_fee_per_gas(client).await.unwrap_err();
// Unimplemented
assert!(is_unimplemented(

View File

@@ -30,8 +30,7 @@ where
/// Returns a suggestion for the priority fee (the tip)
pub(crate) async fn suggested_priority_fee(&self) -> EthResult<U256> {
// TODO: properly implement sampling https://github.com/ethereum/pm/issues/328#issuecomment-853234014
Ok(U256::from(1e9 as u64))
self.gas_oracle().suggest_tip_cap().await
}
/// Reports the fee history, for the given amount of blocks, up until the newest block

View File

@@ -6,6 +6,7 @@
use crate::eth::{
cache::EthStateCache,
error::{EthApiError, EthResult},
gas_oracle::GasPriceOracle,
signer::EthSigner,
};
use async_trait::async_trait;
@@ -71,8 +72,21 @@ pub struct EthApi<Client, Pool, Network> {
impl<Client, Pool, Network> EthApi<Client, Pool, Network> {
/// Creates a new, shareable instance.
pub fn new(client: Client, pool: Pool, network: Network, eth_cache: EthStateCache) -> Self {
let inner = EthApiInner { client, pool, network, signers: Default::default(), eth_cache };
pub fn new(
client: Client,
pool: Pool,
network: Network,
eth_cache: EthStateCache,
gas_oracle: GasPriceOracle<Client>,
) -> Self {
let inner = EthApiInner {
client,
pool,
network,
signers: Default::default(),
eth_cache,
gas_oracle,
};
Self {
inner: Arc::new(inner),
fee_history_cache: FeeHistoryCache::new(
@@ -86,6 +100,11 @@ impl<Client, Pool, Network> EthApi<Client, Pool, Network> {
&self.inner.eth_cache
}
/// Returns the gas oracle frontend
pub(crate) fn gas_oracle(&self) -> &GasPriceOracle<Client> {
&self.inner.gas_oracle
}
/// Returns the inner `Client`
pub fn client(&self) -> &Client {
&self.inner.client
@@ -238,4 +257,6 @@ struct EthApiInner<Client, Pool, Network> {
signers: Vec<Box<dyn EthSigner>>,
/// The async cache frontend for eth related data
eth_cache: EthStateCache,
/// The async gas oracle frontend for gas price suggestions
gas_oracle: GasPriceOracle<Client>,
}

View File

@@ -364,7 +364,10 @@ where
#[cfg(test)]
mod tests {
use crate::{eth::cache::EthStateCache, EthApi};
use crate::{
eth::{cache::EthStateCache, gas_oracle::GasPriceOracle},
EthApi,
};
use jsonrpsee::types::error::INVALID_PARAMS_CODE;
use rand::random;
use reth_network_api::test_utils::NoopNetwork;
@@ -376,11 +379,13 @@ mod tests {
#[tokio::test]
/// Handler for: `eth_test_fee_history`
async fn test_fee_history() {
let cache = EthStateCache::spawn(NoopProvider::default(), Default::default());
let eth_api = EthApi::new(
NoopProvider::default(),
testing_pool(),
NoopNetwork,
EthStateCache::spawn(NoopProvider::default(), Default::default()),
cache.clone(),
GasPriceOracle::new(NoopProvider::default(), Default::default(), cache),
);
let response = <EthApi<_, _, _> as EthApiServer>::fee_history(
@@ -460,11 +465,13 @@ mod tests {
gas_used_ratios.pop();
let cache = EthStateCache::spawn(mock_provider.clone(), Default::default());
let eth_api = EthApi::new(
mock_provider,
mock_provider.clone(),
testing_pool(),
NoopNetwork,
EthStateCache::spawn(NoopProvider::default(), Default::default()),
cache.clone(),
GasPriceOracle::new(mock_provider, Default::default(), cache.clone()),
);
let response = <EthApi<_, _, _> as EthApiServer>::fee_history(

View File

@@ -145,7 +145,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::eth::cache::EthStateCache;
use crate::eth::{cache::EthStateCache, gas_oracle::GasPriceOracle};
use reth_primitives::{StorageKey, StorageValue};
use reth_provider::test_utils::{ExtendedAccount, MockEthProvider, NoopProvider};
use reth_transaction_pool::test_utils::testing_pool;
@@ -156,11 +156,13 @@ mod tests {
// === Noop ===
let pool = testing_pool();
let cache = EthStateCache::spawn(NoopProvider::default(), Default::default());
let eth_api = EthApi::new(
NoopProvider::default(),
pool.clone(),
(),
EthStateCache::spawn(NoopProvider::default(), Default::default()),
cache.clone(),
GasPriceOracle::new(NoopProvider::default(), Default::default(), cache),
);
let address = Address::random();
let storage = eth_api.storage_at(address, U256::ZERO.into(), None).unwrap();
@@ -174,11 +176,13 @@ mod tests {
let account = ExtendedAccount::new(0, U256::ZERO).extend_storage(storage);
mock_provider.add_account(address, account);
let cache = EthStateCache::spawn(mock_provider.clone(), Default::default());
let eth_api = EthApi::new(
mock_provider.clone(),
pool,
(),
EthStateCache::spawn(mock_provider, Default::default()),
cache.clone(),
GasPriceOracle::new(mock_provider, Default::default(), cache),
);
let storage_key: U256 = storage_key.into();

View File

@@ -756,7 +756,10 @@ impl From<TransactionSource> for Transaction {
#[cfg(test)]
mod tests {
use super::*;
use crate::{eth::cache::EthStateCache, EthApi};
use crate::{
eth::{cache::EthStateCache, gas_oracle::GasPriceOracle},
EthApi,
};
use reth_network_api::test_utils::NoopNetwork;
use reth_primitives::{hex_literal::hex, Bytes};
use reth_provider::test_utils::NoopProvider;
@@ -769,11 +772,13 @@ mod tests {
let pool = testing_pool();
let cache = EthStateCache::spawn(noop_provider, Default::default());
let eth_api = EthApi::new(
noop_provider,
pool.clone(),
noop_network_provider,
EthStateCache::spawn(NoopProvider::default(), Default::default()),
cache.clone(),
GasPriceOracle::new(noop_provider, Default::default(), cache),
);
// https://etherscan.io/tx/0xa694b71e6c128a2ed8e2e0f6770bddbe52e3bb8f10e8472f9a79ab81497a8b5d

View File

@@ -0,0 +1,261 @@
//! An implementation of the eth gas price oracle, used for providing gas price estimates based on
//! previous blocks.
use crate::eth::{
cache::EthStateCache,
error::{EthApiError, EthResult, InvalidTransactionError},
};
use reth_primitives::{constants::GWEI_TO_WEI, BlockId, BlockNumberOrTag, H256, U256};
use reth_provider::BlockProviderIdExt;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use tracing::warn;
/// The number of transactions sampled in a block
pub const SAMPLE_NUMBER: u32 = 3;
/// The default maximum gas price to use for the estimate
pub const DEFAULT_MAX_PRICE: U256 = U256::from_limbs([500_000_000_000u64, 0, 0, 0]);
/// The default minimum gas price, under which the sample will be ignored
pub const DEFAULT_IGNORE_PRICE: U256 = U256::from_limbs([2u64, 0, 0, 0]);
/// Settings for the [GasPriceOracle]
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GasPriceOracleConfig {
/// The number of populated blocks to produce the gas price estimate
pub blocks: u32,
/// The percentile of gas prices to use for the estimate
pub percentile: u32,
/// The maximum number of headers to keep in the cache
pub max_header_history: u64,
/// The maximum number of blocks for estimating gas price
pub max_block_history: u64,
/// The default gas price to use if there are no blocks to use
pub default: Option<U256>,
/// The maximum gas price to use for the estimate
pub max_price: Option<U256>,
/// The minimum gas price, under which the sample will be ignored
pub ignore_price: Option<U256>,
}
impl Default for GasPriceOracleConfig {
fn default() -> Self {
GasPriceOracleConfig {
blocks: 20,
percentile: 60,
max_header_history: 1024,
max_block_history: 1024,
default: None,
max_price: Some(DEFAULT_MAX_PRICE),
ignore_price: Some(DEFAULT_IGNORE_PRICE),
}
}
}
/// Calculates a gas price depending on recent blocks.
#[derive(Debug)]
pub struct GasPriceOracle<Client> {
/// The type used to subscribe to block events and get block info
client: Client,
/// The cache for blocks
cache: EthStateCache,
/// The config for the oracle
oracle_config: GasPriceOracleConfig,
/// The latest calculated price and its block hash
last_price: Mutex<GasPriceOracleResult>,
}
impl<Client> GasPriceOracle<Client>
where
Client: BlockProviderIdExt + 'static,
{
/// Creates and returns the [GasPriceOracle].
pub fn new(
client: Client,
mut oracle_config: GasPriceOracleConfig,
cache: EthStateCache,
) -> Self {
// sanitize the perentile to be less than 100
if oracle_config.percentile > 100 {
warn!(prev_percentile=?oracle_config.percentile, "Invalid configured gas price percentile, using 100 instead");
oracle_config.percentile = 100;
}
Self { client, oracle_config, last_price: Default::default(), cache }
}
/// Suggests a gas price estimate based on recent blocks, using the configured percentile.
pub async fn suggest_tip_cap(&self) -> EthResult<U256> {
let block = self
.client
.block_by_id(BlockId::Number(BlockNumberOrTag::Latest))?
.ok_or(EthApiError::UnknownBlockNumber)?;
// seal for the block hash
let header = block.header.seal_slow();
let mut last_price = self.last_price.lock().await;
// if we have stored a last price, then we check whether or not it was for the same head
if last_price.block_hash == header.hash {
return Ok(last_price.price)
}
// if all responses are empty, then we can return a maximum of 2*check_block blocks' worth
// of prices
//
// we only return more than check_block blocks' worth of prices if one or more return empty
// transactions
let mut current_hash = header.hash;
let mut results = Vec::new();
let mut populated_blocks = 0;
// we only check a maximum of 2 * max_block_history, or the number of blocks in the chain
let max_blocks = if self.oracle_config.max_block_history * 2 > header.number {
header.number
} else {
self.oracle_config.max_block_history * 2
};
for _ in 0..max_blocks {
let (parent_hash, block_values) = self
.get_block_values(current_hash, SAMPLE_NUMBER as usize)
.await?
.ok_or(EthApiError::UnknownBlockNumber)?;
if block_values.is_empty() {
results.push(U256::from(last_price.price));
} else {
results.extend(block_values);
populated_blocks += 1;
}
// break when we have enough populated blocks
if populated_blocks >= self.oracle_config.blocks {
break
}
current_hash = parent_hash;
}
// sort results then take the configured percentile result
let mut price = last_price.price;
if !results.is_empty() {
results.sort_unstable();
price = *results
.get((results.len() - 1) * self.oracle_config.percentile as usize / 100)
.expect("gas price index is a percent of nonzero array length, so a value always exists; qed");
}
// constrain to the max price
if let Some(max_price) = self.oracle_config.max_price {
if price > max_price {
price = max_price;
}
}
*last_price = GasPriceOracleResult { block_hash: header.hash, price };
Ok(price)
}
/// Get the `limit` lowest effective tip values for the given block. If the oracle has a
/// configured `ignore_price` threshold, then tip values under that threshold will be ignored
/// before returning a result.
///
/// If the block cannot be found, then this will return `None`.
///
/// This method also returns the parent hash for the given block.
async fn get_block_values(
&self,
block_hash: H256,
limit: usize,
) -> EthResult<Option<(H256, Vec<U256>)>> {
// check the cache (this will hit the disk if the block is not cached)
let block = match self.cache.get_block(block_hash).await? {
Some(block) => block,
None => return Ok(None),
};
// sort the transactions by effective tip
// but first filter those that should be ignored
let txs = block.body.iter();
let mut txs = txs
.filter(|tx| {
if let Some(ignore_under) = self.oracle_config.ignore_price {
if tx.effective_gas_tip(block.base_fee_per_gas).map(U256::from) <
Some(ignore_under)
{
return false
}
}
// recover sender, check if coinbase
let sender = tx.recover_signer();
match sender {
// transactions will be filtered if this is false
Some(addr) => addr != block.beneficiary,
// TODO: figure out an error for this case or ignore
None => false,
}
})
// map all values to effective_gas_tip because we will be returning those values
// anyways
.map(|tx| tx.effective_gas_tip(block.base_fee_per_gas))
.collect::<Vec<_>>();
// now do the sort
txs.sort_unstable();
// fill result with the top `limit` transactions
let mut final_result = Vec::with_capacity(limit);
for tx in txs.iter().take(limit) {
// a `None` effective_gas_tip represents a transaction where the max_fee_per_gas is
// less than the base fee
let effective_tip = tx.ok_or(InvalidTransactionError::FeeCapTooLow)?;
final_result.push(U256::from(effective_tip));
}
Ok(Some((block.parent_hash, final_result)))
}
}
/// Stores the last result that the oracle returned
#[derive(Debug, Clone)]
pub struct GasPriceOracleResult {
/// The block hash that the oracle used to calculate the price
pub block_hash: H256,
/// The price that the oracle calculated
pub price: U256,
}
impl Default for GasPriceOracleResult {
fn default() -> Self {
Self { block_hash: H256::zero(), price: U256::from(GWEI_TO_WEI) }
}
}
#[cfg(test)]
mod tests {
use reth_primitives::constants::GWEI_TO_WEI;
use super::*;
#[test]
fn max_price_sanity() {
assert_eq!(DEFAULT_MAX_PRICE, U256::from(500_000_000_000u64));
assert_eq!(DEFAULT_MAX_PRICE, U256::from(500 * GWEI_TO_WEI))
}
#[test]
fn ignore_price_sanity() {
assert_eq!(DEFAULT_IGNORE_PRICE, U256::from(2u64));
}
}

View File

@@ -4,6 +4,7 @@ mod api;
pub mod cache;
pub mod error;
mod filter;
pub mod gas_oracle;
mod id_provider;
mod logs_utils;
mod pubsub;