pallet_revive_eth_rpc/
fee_history_provider.rs1use crate::{ClientError, FeeHistoryResult, ReceiptInfo, client::SubstrateBlockNumber};
18use pallet_revive_types::runtime_api::BlockV1;
19use sp_core::U256;
20use std::{collections::BTreeMap, sync::Arc};
21use tokio::sync::RwLock;
22
23const CACHE_SIZE: u32 = 1024;
25
26const MAX_REWARD_PERCENTILES: usize = 100;
29
30#[derive(Default, Clone)]
31struct FeeHistoryCacheItem {
32 base_fee: u128,
33 gas_used_ratio: f64,
34 rewards: Vec<u128>,
35}
36
37fn reward_bucket_index(percentile: f64) -> usize {
44 (percentile.clamp(0.0, 100.0) * 2f64).round() as usize
46}
47
48pub(crate) fn validate_reward_percentiles(percentiles: &[f64]) -> Result<(), String> {
55 if percentiles.len() > MAX_REWARD_PERCENTILES {
56 return Err(format!(
57 "invalid reward percentile: over the query limit {MAX_REWARD_PERCENTILES}"
58 ));
59 }
60 for (i, &p) in percentiles.iter().enumerate() {
61 if !(0.0..=100.0).contains(&p) {
62 return Err(format!("invalid reward percentile: {p}"));
63 }
64 if i > 0 && p <= percentiles[i - 1] {
65 return Err(format!(
66 "invalid reward percentile: #{}:{} >= #{}:{}",
67 i - 1,
68 percentiles[i - 1],
69 i,
70 p
71 ));
72 }
73 }
74 Ok(())
75}
76
77#[derive(Default, Clone)]
79pub struct FeeHistoryProvider {
80 fee_history_cache: Arc<RwLock<BTreeMap<SubstrateBlockNumber, FeeHistoryCacheItem>>>,
81}
82
83impl FeeHistoryProvider {
84 pub async fn update_fee_history(&self, block: &BlockV1, receipts: &[ReceiptInfo]) {
86 let reward_percentiles: Vec<f64> = (0..=200).map(|i| i as f64 * 0.5).collect();
91 let block_number: SubstrateBlockNumber =
92 block.number.try_into().expect("Block number is always valid");
93
94 let base_fee = block.base_fee_per_gas.as_u128();
95 let gas_used = block.gas_used.as_u128();
96 let gas_used_ratio = (gas_used as f64) / (block.gas_limit.as_u128() as f64);
97 let mut result = FeeHistoryCacheItem { base_fee, gas_used_ratio, rewards: vec![] };
98
99 let mut receipts = receipts
100 .iter()
101 .map(|receipt| {
102 let gas_used = receipt.gas_used.as_u128();
103 let effective_reward =
104 receipt.effective_gas_price.as_u128().saturating_sub(base_fee);
105 (gas_used, effective_reward)
106 })
107 .collect::<Vec<_>>();
108 receipts.sort_by(|(_, a), (_, b)| a.cmp(b));
109
110 result.rewards = reward_percentiles
112 .into_iter()
113 .filter_map(|p| {
114 let target_gas = (p * gas_used as f64 / 100f64) as u128;
115 let mut sum_gas = 0u128;
116 for (gas_used, reward) in &receipts {
117 sum_gas += gas_used;
118 if target_gas <= sum_gas {
119 return Some(*reward);
120 }
121 }
122 None
123 })
124 .collect();
125
126 let mut cache = self.fee_history_cache.write().await;
127 if cache.len() >= CACHE_SIZE as usize {
128 cache.pop_first();
129 }
130 cache.insert(block_number, result);
131 }
132
133 pub async fn fee_history(
135 &self,
136 block_count: u32,
137 highest: SubstrateBlockNumber,
138 reward_percentiles: Option<Vec<f64>>,
139 ) -> Result<FeeHistoryResult, ClientError> {
140 let block_count = block_count.min(CACHE_SIZE);
141
142 let cache = self.fee_history_cache.read().await;
143 let Some(lowest_in_cache) = cache.first_key_value().map(|(k, _)| *k) else {
144 return Ok(FeeHistoryResult {
145 oldest_block: U256::zero(),
146 base_fee_per_gas: vec![],
147 gas_used_ratio: vec![],
148 reward: vec![],
149 });
150 };
151
152 let lowest = highest
153 .saturating_sub(SubstrateBlockNumber::from(block_count.saturating_sub(1)))
154 .max(lowest_in_cache);
155
156 let mut response = FeeHistoryResult {
157 oldest_block: U256::from(lowest),
158 base_fee_per_gas: Vec::new(),
159 gas_used_ratio: Vec::new(),
160 reward: Default::default(),
161 };
162
163 let rewards = &mut response.reward;
164 for n in lowest..=highest {
166 if let Some(block) = cache.get(&n) {
167 response.base_fee_per_gas.push(U256::from(block.base_fee));
168 response.gas_used_ratio.push(block.gas_used_ratio);
169 if let Some(ref requested_percentiles) = reward_percentiles {
171 let mut block_rewards = Vec::new();
172 for p in requested_percentiles {
174 let reward = if let Some(r) = block.rewards.get(reward_bucket_index(*p)) {
176 U256::from(*r)
177 } else {
178 U256::zero()
179 };
180 block_rewards.push(reward);
181 }
182 if !block_rewards.is_empty() {
184 rewards.push(block_rewards);
185 }
186 }
187 }
188 }
189
190 let base_fee = cache
192 .last_key_value()
193 .map(|(_, block)| U256::from(block.base_fee))
194 .unwrap_or_default();
195 response.base_fee_per_gas.push(base_fee);
196 Ok(response)
197 }
198}
199
200#[tokio::test]
201async fn test_update_fee_history() {
202 let block = BlockV1 {
203 number: U256::from(200u64),
204 base_fee_per_gas: U256::from(1000u64),
205 gas_used: U256::from(600u64),
206 gas_limit: U256::from(1200u64),
207 ..Default::default()
208 };
209
210 let receipts = vec![
211 ReceiptInfo {
212 gas_used: U256::from(200u64),
213 effective_gas_price: U256::from(1200u64),
214 ..Default::default()
215 },
216 ReceiptInfo {
217 gas_used: U256::from(200u64),
218 effective_gas_price: U256::from(1100u64),
219 ..Default::default()
220 },
221 ReceiptInfo {
222 gas_used: U256::from(200u64),
223 effective_gas_price: U256::from(1050u64),
224 ..Default::default()
225 },
226 ];
227
228 let provider = FeeHistoryProvider { fee_history_cache: Arc::new(RwLock::new(BTreeMap::new())) };
229 provider.update_fee_history(&block, &receipts).await;
230
231 let fee_history_result =
232 provider.fee_history(1, 200, Some(vec![0.0f64, 50.0, 100.0])).await.unwrap();
233
234 let expected_result = FeeHistoryResult {
235 oldest_block: U256::from(200),
236 base_fee_per_gas: vec![U256::from(1000), U256::from(1000)],
237 gas_used_ratio: vec![0.5f64],
238 reward: vec![vec![U256::from(50), U256::from(100), U256::from(200)]],
239 };
240 assert_eq!(fee_history_result, expected_result);
241}
242
243#[test]
244fn reward_bucket_index_matches_half_percentile_resolution() {
245 assert_eq!(reward_bucket_index(0.0), 0);
247 assert_eq!(reward_bucket_index(50.0), 100);
248 assert_eq!(reward_bucket_index(100.0), 200);
249
250 assert_eq!(reward_bucket_index(0.5), 1);
252 assert_eq!(reward_bucket_index(50.5), 101);
253
254 assert_eq!(reward_bucket_index(-1.0), 0);
256 assert_eq!(reward_bucket_index(150.0), 200);
257}
258
259#[test]
260fn validate_reward_percentiles_matches_geth() {
261 assert!(validate_reward_percentiles(&[]).is_ok());
263 assert!(validate_reward_percentiles(&[0.0, 20.0, 50.5, 100.0]).is_ok());
264
265 assert!(validate_reward_percentiles(&[-0.1]).is_err());
267 assert!(validate_reward_percentiles(&[100.1]).is_err());
268
269 assert!(validate_reward_percentiles(&[50.0, 20.0]).is_err());
271 assert!(validate_reward_percentiles(&[20.0, 20.0]).is_err());
272
273 let at_limit: Vec<f64> = (0..MAX_REWARD_PERCENTILES).map(|i| i as f64 * 0.5).collect();
275 assert!(validate_reward_percentiles(&at_limit).is_ok());
276 let over_limit: Vec<f64> = (0..=MAX_REWARD_PERCENTILES).map(|i| i as f64 * 0.5).collect();
277 assert!(validate_reward_percentiles(&over_limit).is_err());
278}