diff --git a/crates/networking/rpc/eth/gas_tip_estimator.rs b/crates/networking/rpc/eth/gas_tip_estimator.rs index 4508500641..97fa66dbd9 100644 --- a/crates/networking/rpc/eth/gas_tip_estimator.rs +++ b/crates/networking/rpc/eth/gas_tip_estimator.rs @@ -71,12 +71,19 @@ impl GasTipEstimator { return Err(RpcErr::Internal("Error calculating gas price".to_string())); } let mut results = vec![]; - // TODO: Estimating gas price involves querying multiple blocks - // and doing some calculations with each of them, let's consider - // caching this result, also we can have a specific DB method - // that returns a block range to not query them one-by-one. - for block_num in block_range { - let Some(block_body) = storage.get_block_body(block_num).await? else { + + // Bulk fetch all block bodies and headers for the range + let bodies = storage + .get_block_bodies(block_range_lower_bound, latest_block_number) + .await?; + let headers = storage + .get_block_headers(block_range_lower_bound, latest_block_number) + .await?; + + for (idx, (block_body, header)) in bodies.into_iter().zip(headers.into_iter()).enumerate() { + let block_num = block_range_lower_bound + idx as u64; + + let Some(block_body) = block_body else { error!( "Block body for block number {block_num} is missing but is below the latest known block!" ); @@ -85,11 +92,8 @@ impl GasTipEstimator { )); }; - let base_fee = storage - .get_block_header(block_num) - .ok() - .flatten() - .and_then(|header| header.base_fee_per_gas); + // Get the base fee for the block + let base_fee = header.and_then(|h| h.base_fee_per_gas); // Previously we took the gas_price, now we take the effective_gas_tip and add the base_fee in the RPC // call if needed. diff --git a/crates/storage/api.rs b/crates/storage/api.rs index 6de757324a..596680b7f0 100644 --- a/crates/storage/api.rs +++ b/crates/storage/api.rs @@ -37,6 +37,13 @@ pub trait StoreEngine: Debug + Send + Sync + RefUnwindSafe { block_number: BlockNumber, ) -> Result, StoreError>; + /// Obtain canonical block headers in from..=to + async fn get_block_headers( + &self, + from: BlockNumber, + to: BlockNumber, + ) -> Result>, StoreError>; + /// Add block body async fn add_block_body( &self, diff --git a/crates/storage/store.rs b/crates/storage/store.rs index b73d6d726b..856f12acb3 100644 --- a/crates/storage/store.rs +++ b/crates/storage/store.rs @@ -189,6 +189,14 @@ impl Store { self.engine.get_block_header(block_number) } + pub async fn get_block_headers( + &self, + from: BlockNumber, + to: BlockNumber, + ) -> Result>, StoreError> { + self.engine.get_block_headers(from, to).await + } + pub fn get_block_header_by_hash( &self, block_hash: BlockHash, @@ -1502,6 +1510,7 @@ mod tests { run_test(test_genesis_block, engine_type).await; run_test(test_iter_accounts, engine_type).await; run_test(test_iter_storage, engine_type).await; + run_test(test_get_block_headers_and_bodies_bulk, engine_type).await; } async fn test_iter_accounts(store: Store) { @@ -1626,6 +1635,68 @@ mod tests { assert_eq!(stored_body, block_body); } + async fn test_get_block_headers_and_bodies_bulk(store: Store) { + // Create and store multiple blocks + let mut headers = Vec::new(); + let mut bodies = Vec::new(); + + for i in 0..5u64 { + let (mut header, body) = create_block_for_testing(); + header.number = i; + // Make them unique by modifying the timestamp + header.timestamp = 1000 + i; + let hash = header.hash(); + + store.add_block_header(hash, header.clone()).await.unwrap(); + store.add_block_body(hash, body.clone()).await.unwrap(); + store + .forkchoice_update(None, i, hash, None, None) + .await + .unwrap(); + + headers.push(header); + bodies.push(body); + } + + // Test bulk fetch of headers + let fetched_headers = store.get_block_headers(0, 4).await.unwrap(); + assert_eq!(fetched_headers.len(), 5); + for (i, fetched) in fetched_headers.into_iter().enumerate() { + let fetched = fetched.expect("header should exist"); + // Ensure hashes are computed for comparison + let _ = fetched.hash(); + let _ = headers[i].hash(); + assert_eq!(fetched, headers[i]); + } + + // Test bulk fetch of bodies + let fetched_bodies = store.get_block_bodies(0, 4).await.unwrap(); + assert_eq!(fetched_bodies.len(), 5); + for (i, fetched) in fetched_bodies.into_iter().enumerate() { + let fetched = fetched.expect("body should exist"); + assert_eq!(fetched, bodies[i]); + } + + // Test partial range + let partial_headers = store.get_block_headers(2, 4).await.unwrap(); + assert_eq!(partial_headers.len(), 3); + for (i, fetched) in partial_headers.into_iter().enumerate() { + let fetched = fetched.expect("header should exist"); + let _ = fetched.hash(); + let _ = headers[i + 2].hash(); + assert_eq!(fetched, headers[i + 2]); + } + + // Test range with missing blocks (beyond stored range) + let extended_headers = store.get_block_headers(3, 7).await.unwrap(); + assert_eq!(extended_headers.len(), 5); + assert!(extended_headers[0].is_some()); + assert!(extended_headers[1].is_some()); + assert!(extended_headers[2].is_none()); + assert!(extended_headers[3].is_none()); + assert!(extended_headers[4].is_none()); + } + fn create_block_for_testing() -> (BlockHeader, BlockBody) { let block_header = BlockHeader { parent_hash: H256::from_str( diff --git a/crates/storage/store_db/in_memory.rs b/crates/storage/store_db/in_memory.rs index 2ee71740be..80720781ff 100644 --- a/crates/storage/store_db/in_memory.rs +++ b/crates/storage/store_db/in_memory.rs @@ -187,6 +187,23 @@ impl StoreEngine for Store { } } + async fn get_block_headers( + &self, + from: BlockNumber, + to: BlockNumber, + ) -> Result>, StoreError> { + let store = self.inner()?; + let mut res = Vec::new(); + for block_number in from..=to { + let header_opt = store + .canonical_hashes + .get(&block_number) + .and_then(|hash| store.headers.get(hash)); + res.push(header_opt.cloned()); + } + Ok(res) + } + async fn get_block_body(&self, block_number: u64) -> Result, StoreError> { let store = self.inner()?; if let Some(hash) = store.canonical_hashes.get(&block_number) { diff --git a/crates/storage/store_db/rocksdb.rs b/crates/storage/store_db/rocksdb.rs index 6637780317..2feb7c8b24 100644 --- a/crates/storage/store_db/rocksdb.rs +++ b/crates/storage/store_db/rocksdb.rs @@ -1102,6 +1102,55 @@ impl StoreEngine for Store { self.get_block_header_by_hash(block_hash) } + async fn get_block_headers( + &self, + from: BlockNumber, + to: BlockNumber, + ) -> Result>, StoreError> { + let numbers: Vec = (from..=to).collect(); + let number_keys: Vec> = numbers.iter().map(|n| n.to_le_bytes().to_vec()).collect(); + + let hashes = self + .read_bulk_async(CF_CANONICAL_BLOCK_HASHES, number_keys, |bytes| { + BlockHashRLP::from_bytes(bytes) + .to() + .map_err(StoreError::from) + }) + .await?; + + let hash_keys: Vec> = hashes + .iter() + .flat_map(|hash_opt| hash_opt.map(|h| BlockHashRLP::from(h).bytes().clone())) + .collect(); + + let mut headers = self + .read_bulk_async(CF_HEADERS, hash_keys, |bytes| { + BlockHeaderRLP::from_bytes(bytes) + .to() + .map_err(StoreError::from) + }) + .await?; + + let mut i = 0; + + // Fill in with None for missing headers + let headers = hashes + .into_iter() + .map(|opt| { + opt.and_then(|_| { + let header_ref = headers + .get_mut(i) + .expect("headers length is equal to number of Somes in hashes"); + let header = std::mem::take(header_ref); + i += 1; + header + }) + }) + .collect(); + + Ok(headers) + } + async fn add_block_body( &self, block_hash: BlockHash,