1use alloy_consensus::BlockHeader;
2use alloy_eips::BlockHashOrNumber;
3use alloy_primitives::{BlockHash, BlockNumber, Sealable, B256};
4use async_compression::tokio::bufread::GzipDecoder;
5use futures::Future;
6use itertools::{Either, Itertools};
7use reth_consensus::{Consensus, ConsensusError};
8use reth_network_p2p::{
9 bodies::client::{BodiesClient, BodiesFut},
10 download::DownloadClient,
11 error::RequestError,
12 headers::client::{HeadersClient, HeadersDirection, HeadersFut, HeadersRequest},
13 priority::Priority,
14 BlockClient,
15};
16use reth_network_peers::PeerId;
17use reth_primitives_traits::{Block, BlockBody, FullBlock, SealedBlock, SealedHeader};
18use std::{collections::HashMap, io, ops::RangeInclusive, path::Path, sync::Arc};
19use thiserror::Error;
20use tokio::{
21 fs::File,
22 io::{AsyncReadExt, BufReader},
23};
24use tokio_stream::StreamExt;
25use tokio_util::codec::FramedRead;
26use tracing::{debug, trace, warn};
27
28use super::file_codec::BlockFileCodec;
29use crate::receipt_file_client::FromReceiptReader;
30
31pub const DEFAULT_BYTE_LEN_CHUNK_CHAIN_FILE: u64 = 1_000_000_000;
35
36#[derive(Debug, Clone)]
48pub struct FileClient<B: Block> {
49 headers: HashMap<BlockNumber, B::Header>,
51
52 hash_to_number: HashMap<BlockHash, BlockNumber>,
54
55 bodies: HashMap<BlockHash, B::Body>,
57}
58
59#[derive(Debug, Error)]
61pub enum FileClientError {
62 #[error(transparent)]
64 Consensus(#[from] ConsensusError),
65
66 #[error(transparent)]
68 Io(#[from] std::io::Error),
69
70 #[error("{0}")]
72 Rlp(alloy_rlp::Error, Vec<u8>),
73
74 #[error("{0}")]
76 Custom(&'static str),
77}
78
79impl From<&'static str> for FileClientError {
80 fn from(value: &'static str) -> Self {
81 Self::Custom(value)
82 }
83}
84
85impl<B: FullBlock> FileClient<B> {
86 pub fn from_blocks(blocks: impl IntoIterator<Item = SealedBlock<B>>) -> Self {
88 let blocks: Vec<_> = blocks.into_iter().collect();
89 let capacity = blocks.len();
90
91 let mut headers = HashMap::with_capacity(capacity);
92 let mut hash_to_number = HashMap::with_capacity(capacity);
93 let mut bodies = HashMap::with_capacity(capacity);
94
95 for block in blocks {
96 let number = block.number();
97 let hash = block.hash();
98 let (header, body) = block.split_sealed_header_body();
99
100 headers.insert(number, header.into_header());
101 hash_to_number.insert(hash, number);
102 bodies.insert(hash, body);
103 }
104
105 Self { headers, hash_to_number, bodies }
106 }
107
108 pub async fn new<P: AsRef<Path>>(
110 path: P,
111 consensus: Arc<dyn Consensus<B>>,
112 ) -> Result<Self, FileClientError> {
113 let file = File::open(path).await?;
114 Self::from_file(file, consensus).await
115 }
116
117 pub(crate) async fn from_file(
119 mut file: File,
120 consensus: Arc<dyn Consensus<B>>,
121 ) -> Result<Self, FileClientError> {
122 let metadata = file.metadata().await?;
124 let file_len = metadata.len();
125
126 let mut reader = vec![];
127 file.read_to_end(&mut reader).await?;
128
129 Ok(FileClientBuilder { consensus, parent_header: None, skip_invalid_blocks: false }
130 .build(&reader[..], file_len)
131 .await?
132 .file_client)
133 }
134
135 pub fn tip(&self) -> Option<B256> {
137 self.headers.get(&self.max_block()?).map(|h| h.hash_slow())
138 }
139
140 pub fn start(&self) -> Option<B256> {
142 self.headers.get(&self.min_block()?).map(|h| h.hash_slow())
143 }
144
145 pub fn max_block(&self) -> Option<u64> {
147 self.headers.keys().max().copied()
148 }
149
150 pub fn min_block(&self) -> Option<u64> {
152 self.headers.keys().min().copied()
153 }
154
155 pub fn tip_header(&self) -> Option<SealedHeader<B::Header>> {
158 self.headers.get(&self.max_block()?).map(|h| SealedHeader::seal_slow(h.clone()))
159 }
160
161 pub fn has_canonical_blocks(&self) -> bool {
163 if self.headers.is_empty() {
164 return true
165 }
166 let (min, max) = self.headers.keys().minmax().into_option().expect("not empty");
167 *max - *min + 1 == self.headers.len() as u64
169 }
170
171 pub fn with_bodies(mut self, bodies: HashMap<BlockHash, B::Body>) -> Self {
173 self.bodies = bodies;
174 self
175 }
176
177 pub fn with_headers(mut self, headers: HashMap<BlockNumber, B::Header>) -> Self {
179 self.headers = headers;
180 for (number, header) in &self.headers {
181 self.hash_to_number.insert(header.hash_slow(), *number);
182 }
183 self
184 }
185
186 pub fn headers_len(&self) -> usize {
188 self.headers.len()
189 }
190
191 pub fn bodies_len(&self) -> usize {
193 self.bodies.len()
194 }
195
196 pub fn headers_iter(&self) -> impl Iterator<Item = &B::Header> {
198 self.headers.values()
199 }
200
201 pub fn bodies_iter_mut(&mut self) -> impl Iterator<Item = (u64, &mut B::Body)> {
205 let bodies = &mut self.bodies;
206 let numbers = &self.hash_to_number;
207 bodies.iter_mut().map(|(hash, body)| (numbers[hash], body))
208 }
209
210 pub fn total_transactions(&self) -> usize {
212 self.bodies.iter().fold(0, |acc, (_, body)| acc + body.transactions().len())
213 }
214}
215
216struct FileClientBuilder<B: Block> {
217 pub consensus: Arc<dyn Consensus<B>>,
218 pub parent_header: Option<SealedHeader<B::Header>>,
219 pub skip_invalid_blocks: bool,
220}
221
222impl<B: FullBlock<Header: reth_primitives_traits::BlockHeader>> FromReader
223 for FileClientBuilder<B>
224{
225 type Error = FileClientError;
226 type Output = FileClient<B>;
227
228 fn build<R>(
230 &self,
231 reader: R,
232 num_bytes: u64,
233 ) -> impl Future<Output = Result<DecodedFileChunk<Self::Output>, Self::Error>>
234 where
235 R: AsyncReadExt + Unpin,
236 {
237 let mut headers = HashMap::default();
238 let mut hash_to_number = HashMap::default();
239 let mut bodies = HashMap::default();
240
241 let mut stream =
243 FramedRead::with_capacity(reader, BlockFileCodec::<B>::default(), num_bytes as usize);
244
245 trace!(target: "downloaders::file",
246 target_num_bytes=num_bytes,
247 capacity=stream.read_buffer().capacity(),
248 "init decode stream"
249 );
250
251 let mut remaining_bytes = vec![];
252
253 let mut log_interval = 0;
254 let mut log_interval_start_block = 0;
255
256 let mut parent_header = self.parent_header.clone();
257
258 async move {
259 while let Some(block_res) = stream.next().await {
260 let block = match block_res {
261 Ok(block) => block,
262 Err(FileClientError::Rlp(err, bytes)) => {
263 trace!(target: "downloaders::file",
264 %err,
265 bytes_len=bytes.len(),
266 "partial block returned from decoding chunk"
267 );
268 remaining_bytes = bytes;
269 break
270 }
271 Err(err) => return Err(err),
272 };
273
274 let block = SealedBlock::seal_slow(block);
275
276 let validation =
281 self.consensus.validate_header(block.sealed_header()).and_then(|_| {
282 if let Some(parent) = &parent_header {
283 self.consensus
284 .validate_header_against_parent(block.sealed_header(), parent)?;
285 }
286 self.consensus.validate_block_pre_execution(&block)
287 });
288 if let Err(err) = validation {
289 if !self.skip_invalid_blocks {
290 return Err(err.into())
291 }
292 warn!(target: "downloaders::file",
293 block_number = block.number(),
294 block_hash = %block.hash(),
295 %err,
296 "skipping invalid block while decoding file"
297 );
298 continue
299 }
300 if parent_header.is_some() {
301 parent_header = Some(block.sealed_header().clone());
302 }
303
304 let block_hash = block.hash();
306 let block_number = block.number();
307 let (header, body) = block.split_sealed_header_body();
308 headers.insert(block_number, header.unseal());
309 hash_to_number.insert(block_hash, block_number);
310 bodies.insert(block_hash, body);
311
312 if log_interval == 0 {
313 trace!(target: "downloaders::file",
314 block_number,
315 "read first block"
316 );
317 log_interval_start_block = block_number;
318 } else if log_interval % 100_000 == 0 {
319 trace!(target: "downloaders::file",
320 blocks=?log_interval_start_block..=block_number,
321 "read blocks from file"
322 );
323 log_interval_start_block = block_number + 1;
324 }
325 log_interval += 1;
326 }
327
328 trace!(target: "downloaders::file", blocks = headers.len(), "Initialized file client");
329
330 Ok(DecodedFileChunk {
331 file_client: FileClient { headers, hash_to_number, bodies },
332 remaining_bytes,
333 highest_block: None,
334 })
335 }
336 }
337}
338
339impl<B: FullBlock> HeadersClient for FileClient<B> {
340 type Header = B::Header;
341 type Output = HeadersFut<B::Header>;
342
343 fn get_headers_with_priority(
344 &self,
345 request: HeadersRequest,
346 _priority: Priority,
347 ) -> Self::Output {
348 let mut headers = Vec::new();
350 trace!(target: "downloaders::file", request=?request, "Getting headers");
351
352 let start_num = match request.start {
353 BlockHashOrNumber::Hash(hash) => match self.hash_to_number.get(&hash) {
354 Some(num) => *num,
355 None => {
356 warn!(%hash, "Could not find starting block number for requested header hash");
357 return Box::pin(async move { Err(RequestError::BadResponse) })
358 }
359 },
360 BlockHashOrNumber::Number(num) => num,
361 };
362
363 let range = if request.limit == 1 {
364 Either::Left(start_num..start_num + 1)
365 } else {
366 match request.direction {
367 HeadersDirection::Rising => Either::Left(start_num..start_num + request.limit),
368 HeadersDirection::Falling => {
369 Either::Right((start_num - request.limit + 1..=start_num).rev())
370 }
371 }
372 };
373
374 trace!(target: "downloaders::file", range=?range, "Getting headers with range");
375
376 for block_number in range {
377 match self.headers.get(&block_number).cloned() {
378 Some(header) => headers.push(header),
379 None => {
380 warn!(number=%block_number, "Could not find header");
381 return Box::pin(async move { Err(RequestError::BadResponse) })
382 }
383 }
384 }
385
386 Box::pin(async move { Ok((PeerId::default(), headers).into()) })
387 }
388}
389
390impl<B: FullBlock> BodiesClient for FileClient<B> {
391 type Body = B::Body;
392 type Output = BodiesFut<B::Body>;
393
394 fn get_block_bodies_with_priority_and_range_hint(
395 &self,
396 hashes: Vec<B256>,
397 _priority: Priority,
398 _range_hint: Option<RangeInclusive<u64>>,
399 ) -> Self::Output {
400 let mut bodies = Vec::new();
402
403 for hash in hashes {
406 match self.bodies.get(&hash).cloned() {
407 Some(body) => bodies.push(body),
408 None => return Box::pin(async move { Err(RequestError::BadResponse) }),
409 }
410 }
411
412 Box::pin(async move { Ok((PeerId::default(), bodies).into()) })
413 }
414}
415
416impl<B: FullBlock> DownloadClient for FileClient<B> {
417 fn report_bad_message(&self, _peer_id: PeerId) {
418 trace!("Reported a bad message on a file client, the file may be corrupted or invalid");
419 }
421
422 fn num_connected_peers(&self) -> usize {
423 1
425 }
426}
427
428impl<B: FullBlock> BlockClient for FileClient<B> {
429 type Block = B;
430}
431
432#[derive(Debug)]
434enum FileReader {
435 Plain { file: File, remaining_bytes: u64 },
437 Gzip { decoder: GzipDecoder<BufReader<File>>, eof: bool },
439}
440
441impl FileReader {
442 async fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> {
444 match self {
445 Self::Plain { file, .. } => file.read(buf).await,
446 Self::Gzip { decoder, .. } => decoder.read(buf).await,
447 }
448 }
449
450 const fn is_eof(&self) -> bool {
451 match self {
452 Self::Plain { remaining_bytes, .. } => *remaining_bytes == 0,
453 Self::Gzip { eof, .. } => *eof,
454 }
455 }
456
457 async fn read_next_chunk(
460 &mut self,
461 chunk: &mut Vec<u8>,
462 chunk_byte_len: u64,
463 ) -> Result<Option<u64>, FileClientError> {
464 match self {
465 Self::Plain { .. } => self.read_plain_chunk(chunk, chunk_byte_len).await,
466 Self::Gzip { .. } => {
467 Ok((self.read_gzip_chunk(chunk, chunk_byte_len).await?)
468 .then_some(chunk.len() as u64))
469 }
470 }
471 }
472
473 async fn read_plain_chunk(
474 &mut self,
475 chunk: &mut Vec<u8>,
476 chunk_byte_len: u64,
477 ) -> Result<Option<u64>, FileClientError> {
478 let Self::Plain { file, remaining_bytes } = self else {
479 unreachable!("read_plain_chunk should only be called on Plain variant")
480 };
481
482 if *remaining_bytes == 0 && chunk.is_empty() {
483 return Ok(None)
485 }
486
487 let chunk_target_len = chunk_byte_len.min(*remaining_bytes + chunk.len() as u64);
488 let old_bytes_len = chunk.len() as u64;
489
490 let new_read_bytes_target_len = chunk_target_len - old_bytes_len;
492
493 let prev_read_bytes_len = chunk.len();
495 chunk.extend(std::iter::repeat_n(0, new_read_bytes_target_len as usize));
496 let reader = &mut chunk[prev_read_bytes_len..];
497
498 let new_read_bytes_len = file.read_exact(reader).await? as u64;
500 let next_chunk_byte_len = chunk.len();
501
502 *remaining_bytes -= new_read_bytes_len;
504
505 debug!(target: "downloaders::file",
506 max_chunk_byte_len=chunk_byte_len,
507 prev_read_bytes_len,
508 new_read_bytes_target_len,
509 new_read_bytes_len,
510 next_chunk_byte_len,
511 remaining_file_byte_len=*remaining_bytes,
512 "new bytes were read from file"
513 );
514
515 Ok(Some(next_chunk_byte_len as u64))
516 }
517
518 async fn read_gzip_chunk(
520 &mut self,
521 chunk: &mut Vec<u8>,
522 chunk_byte_len: u64,
523 ) -> Result<bool, FileClientError> {
524 let mut buffer = vec![0u8; 64 * 1024];
525 loop {
526 if chunk.len() >= chunk_byte_len as usize {
527 return Ok(true)
528 }
529
530 match self.read(&mut buffer).await {
531 Ok(0) => {
532 let Self::Gzip { eof, .. } = self else { unreachable!() };
533 *eof = true;
534 return Ok(!chunk.is_empty())
535 }
536 Ok(n) => {
537 chunk.extend_from_slice(&buffer[..n]);
538 }
539 Err(e) => return Err(e.into()),
540 }
541 }
542 }
543}
544
545#[derive(Debug)]
547pub struct ChunkedFileReader {
548 file: FileReader,
550 chunk: Vec<u8>,
552 chunk_byte_len: u64,
554 highest_block: Option<u64>,
557}
558
559impl ChunkedFileReader {
560 pub async fn new<P: AsRef<Path>>(
564 path: P,
565 chunk_byte_len: Option<u64>,
566 ) -> Result<Self, FileClientError> {
567 let path = path.as_ref();
568 let file = File::open(path).await?;
569 let chunk_byte_len = chunk_byte_len.unwrap_or(DEFAULT_BYTE_LEN_CHUNK_CHAIN_FILE);
570
571 Self::from_file(
572 file,
573 chunk_byte_len,
574 path.extension()
575 .and_then(|ext| ext.to_str())
576 .is_some_and(|ext| ["gz", "gzip"].contains(&ext)),
577 )
578 .await
579 }
580
581 pub async fn from_file(
583 file: File,
584 chunk_byte_len: u64,
585 is_gzip: bool,
586 ) -> Result<Self, FileClientError> {
587 let file_reader = if is_gzip {
588 FileReader::Gzip { decoder: GzipDecoder::new(BufReader::new(file)), eof: false }
589 } else {
590 let remaining_bytes = file.metadata().await?.len();
591 FileReader::Plain { file, remaining_bytes }
592 };
593
594 Ok(Self { file: file_reader, chunk: vec![], chunk_byte_len, highest_block: None })
595 }
596
597 async fn read_next_chunk(&mut self) -> Result<Option<u64>, FileClientError> {
600 self.file.read_next_chunk(&mut self.chunk, self.chunk_byte_len).await
601 }
602
603 pub async fn next_chunk<B: FullBlock>(
608 &mut self,
609 consensus: Arc<dyn Consensus<B>>,
610 parent_header: Option<SealedHeader<B::Header>>,
611 ) -> Result<Option<FileClient<B>>, FileClientError> {
612 self.next_chunk_with_invalid_block_handling(consensus, parent_header, false).await
613 }
614
615 pub async fn next_chunk_with_invalid_block_handling<B: FullBlock>(
617 &mut self,
618 consensus: Arc<dyn Consensus<B>>,
619 parent_header: Option<SealedHeader<B::Header>>,
620 skip_invalid_blocks: bool,
621 ) -> Result<Option<FileClient<B>>, FileClientError> {
622 let Some(chunk_len) = self.read_next_chunk().await? else { return Ok(None) };
623
624 let DecodedFileChunk { file_client, remaining_bytes, .. } =
626 FileClientBuilder { consensus, parent_header, skip_invalid_blocks }
627 .build(&self.chunk[..], chunk_len)
628 .await?;
629
630 if self.file.is_eof() && !remaining_bytes.is_empty() {
631 return Err(FileClientError::Rlp(alloy_rlp::Error::InputTooShort, remaining_bytes))
632 }
633
634 self.chunk = remaining_bytes;
636
637 Ok(Some(file_client))
638 }
639
640 pub async fn next_receipts_chunk<T>(&mut self) -> Result<Option<T>, T::Error>
642 where
643 T: FromReceiptReader,
644 {
645 let Some(next_chunk_byte_len) = self.read_next_chunk().await.map_err(|e| {
646 T::Error::from(match e {
647 FileClientError::Io(io_err) => io_err,
648 _ => io::Error::other(e.to_string()),
649 })
650 })?
651 else {
652 return Ok(None)
653 };
654
655 let DecodedFileChunk { file_client, remaining_bytes, highest_block } =
657 T::from_receipt_reader(&self.chunk[..], next_chunk_byte_len, self.highest_block)
658 .await?;
659
660 self.chunk = remaining_bytes;
662 self.highest_block = highest_block;
664
665 Ok(Some(file_client))
666 }
667}
668
669pub trait FromReader {
671 type Error: From<io::Error>;
673
674 type Output;
676
677 fn build<R>(
679 &self,
680 reader: R,
681 num_bytes: u64,
682 ) -> impl Future<Output = Result<DecodedFileChunk<Self::Output>, Self::Error>>
683 where
684 Self: Sized,
685 R: AsyncReadExt + Unpin;
686}
687
688#[derive(Debug)]
690pub struct DecodedFileChunk<T> {
691 pub file_client: T,
693 pub remaining_bytes: Vec<u8>,
695 pub highest_block: Option<u64>,
698}
699
700#[cfg(test)]
701mod tests {
702 use super::*;
703 use crate::{
704 bodies::{
705 bodies::BodiesDownloaderBuilder,
706 test_utils::{insert_headers, zip_blocks},
707 },
708 headers::{reverse_headers::ReverseHeadersDownloaderBuilder, test_utils::child_header},
709 test_utils::{generate_bodies, generate_bodies_file},
710 };
711 use assert_matches::assert_matches;
712 use async_compression::tokio::write::GzipEncoder;
713 use futures_util::stream::StreamExt;
714 use rand::Rng;
715 use reth_consensus::{noop::NoopConsensus, test_utils::TestConsensus, ConsensusError};
716 use reth_ethereum_primitives::Block;
717 use reth_network_p2p::{
718 bodies::downloader::BodyDownloader,
719 headers::downloader::{HeaderDownloader, SyncTarget},
720 };
721 use reth_provider::test_utils::create_test_provider_factory;
722 use std::sync::Arc;
723 use tokio::{
724 fs::File,
725 io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, SeekFrom},
726 };
727
728 #[tokio::test]
729 async fn streams_bodies_from_buffer() {
730 let factory = create_test_provider_factory();
732 let (headers, mut bodies) = generate_bodies(0..=19);
733
734 insert_headers(&factory, &headers);
735
736 let file = tempfile::tempfile().unwrap();
738
739 let client: Arc<FileClient<Block>> = Arc::new(
740 FileClient::from_file(file.into(), NoopConsensus::arc())
741 .await
742 .unwrap()
743 .with_bodies(bodies.clone().into_iter().collect()),
744 );
745 let mut downloader = BodiesDownloaderBuilder::default().build::<Block, _, _>(
746 client.clone(),
747 Arc::new(TestConsensus::default()),
748 factory,
749 );
750 downloader.set_download_range(0..=19).expect("failed to set download range");
751
752 assert_matches!(
753 downloader.next().await,
754 Some(Ok(res)) => assert_eq!(res, zip_blocks(headers.iter(), &mut bodies))
755 );
756 }
757
758 #[tokio::test]
759 async fn download_headers_at_fork_head() {
760 reth_tracing::init_test_tracing();
761
762 let p3 = SealedHeader::default();
763 let p2 = child_header(&p3);
764 let p1 = child_header(&p2);
765 let p0 = child_header(&p1);
766
767 let file = tempfile::tempfile().unwrap();
768 let client: Arc<FileClient<Block>> = Arc::new(
769 FileClient::from_file(file.into(), NoopConsensus::arc()).await.unwrap().with_headers(
770 HashMap::from([
771 (0u64, p0.clone_header()),
772 (1, p1.clone_header()),
773 (2, p2.clone_header()),
774 (3, p3.clone_header()),
775 ]),
776 ),
777 );
778
779 let mut downloader = ReverseHeadersDownloaderBuilder::default()
780 .stream_batch_size(3)
781 .request_limit(3)
782 .build(Arc::clone(&client), Arc::new(TestConsensus::default()));
783 downloader.update_local_head(p3.clone());
784 downloader.update_sync_target(SyncTarget::Tip(p0.hash()));
785
786 let headers = downloader.next().await.unwrap();
787 assert_eq!(headers.unwrap(), vec![p0, p1, p2]);
788 assert!(downloader.next().await.is_none());
789 assert!(downloader.next().await.is_none());
790 }
791
792 #[tokio::test]
793 async fn test_download_headers_from_file() {
794 reth_tracing::init_test_tracing();
795
796 let (file, headers, _) = generate_bodies_file(0..=19).await;
798 let client: Arc<FileClient<Block>> =
800 Arc::new(FileClient::from_file(file, NoopConsensus::arc()).await.unwrap());
801
802 let mut header_downloader = ReverseHeadersDownloaderBuilder::default()
804 .build(Arc::clone(&client), Arc::new(TestConsensus::default()));
805 header_downloader.update_local_head(headers.first().unwrap().clone());
806 header_downloader.update_sync_target(SyncTarget::Tip(headers.last().unwrap().hash()));
807
808 let mut downloaded_headers = header_downloader.next().await.unwrap().unwrap();
810
811 downloaded_headers.reverse();
813
814 assert_eq!(downloaded_headers, headers[1..]);
816 }
817
818 #[tokio::test]
819 async fn test_download_bodies_from_file() {
820 let factory = create_test_provider_factory();
822 let (file, headers, mut bodies) = generate_bodies_file(0..=19).await;
823
824 let client: Arc<FileClient<Block>> =
826 Arc::new(FileClient::from_file(file, NoopConsensus::arc()).await.unwrap());
827
828 insert_headers(&factory, &headers);
830
831 let mut downloader = BodiesDownloaderBuilder::default().build::<Block, _, _>(
832 client.clone(),
833 Arc::new(TestConsensus::default()),
834 factory,
835 );
836 downloader.set_download_range(0..=19).expect("failed to set download range");
837
838 assert_matches!(
839 downloader.next().await,
840 Some(Ok(res)) => assert_eq!(res, zip_blocks(headers.iter(), &mut bodies))
841 );
842 }
843
844 #[tokio::test]
845 async fn strict_chunk_decode_fails_on_invalid_block() {
846 let (file, _, _) = generate_bodies_file(0..=2).await;
847 let chunk_byte_len = file.metadata().await.unwrap().len();
848 let mut reader = ChunkedFileReader::from_file(file, chunk_byte_len, false).await.unwrap();
849 let consensus = Arc::new(TestConsensus::default());
850 consensus.set_fail_validation(true);
851
852 let err = reader.next_chunk::<Block>(consensus, None).await.unwrap_err();
853
854 assert_matches!(err, FileClientError::Consensus(ConsensusError::BaseFeeMissing));
855 }
856
857 #[tokio::test]
858 async fn lenient_chunk_decode_skips_invalid_blocks() {
859 let (file, _, _) = generate_bodies_file(0..=2).await;
860 let chunk_byte_len = file.metadata().await.unwrap().len();
861 let mut reader = ChunkedFileReader::from_file(file, chunk_byte_len, false).await.unwrap();
862 let consensus = Arc::new(TestConsensus::default());
863 consensus.set_fail_validation(true);
864
865 let client = reader
866 .next_chunk_with_invalid_block_handling::<Block>(consensus, None, true)
867 .await
868 .unwrap()
869 .unwrap();
870
871 assert_eq!(client.headers_len(), 0);
872 assert!(client.tip().is_none());
873 }
874
875 #[tokio::test]
876 async fn trailing_transaction_data_at_eof_is_rejected() {
877 let block = alloy_primitives::hex!(
878 "f902cef90259a01f1e77fa4e08a5ce98ba78db75ca1a4623c10e832eeff5a4a770e9d5048bfa95a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794000000000000000000000000000000000000c0fea059eb85c4cc1486f67674192abb9fff7ae7f38f2ecdd3c87458ae2b8462eb5ca2a0a79a055a833e5c8e9364a9f6f06e1e01856d25a3cc21bad067cd94eb5cf9c7e9a0f78dfb743fbd92ade140711c8bbc542b5e307f0ab7984eff35d751969fe57efab901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080018401c9c3808252080c80a000000000000000000000000000000000000000000000000000000000000000008800000000000000000aa056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b4218080a00000000000000000000000000000000000000000000000000000000000000000a0e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855f86eb86c02f8680180830f4240830f424082520894000000000000000000000000000000000000c0de8080c001a079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798a03d1613cba75c9e7513aee78156909bdb32050830f2faa3125e8fd64b5778be3000c0c0"
879 );
880 let mut file = File::from_std(tempfile::tempfile().unwrap());
881 file.write_all(&block).await.unwrap();
882 file.seek(SeekFrom::Start(0)).await.unwrap();
883 let mut reader =
884 ChunkedFileReader::from_file(file, block.len() as u64, false).await.unwrap();
885
886 let err = reader.next_chunk::<Block>(NoopConsensus::arc(), None).await.unwrap_err();
887
888 assert_matches!(
889 err,
890 FileClientError::Rlp(alloy_rlp::Error::InputTooShort, bytes) if bytes == block
891 );
892 }
893
894 #[tokio::test]
895 async fn test_chunk_download_headers_from_file() {
896 reth_tracing::init_test_tracing();
897
898 let (file, headers, _) = generate_bodies_file(0..=14).await;
900
901 let chunk_byte_len = rand::rng().random_range(2000..=10_000);
904 trace!(target: "downloaders::file::test", chunk_byte_len);
905
906 let mut reader =
908 ChunkedFileReader::from_file(file, chunk_byte_len as u64, false).await.unwrap();
909
910 let mut downloaded_headers: Vec<SealedHeader> = vec![];
911
912 let mut local_header = headers.first().unwrap().clone();
913
914 while let Some(client) =
916 reader.next_chunk::<Block>(NoopConsensus::arc(), None).await.unwrap()
917 {
918 let sync_target = client.tip_header().unwrap();
919
920 let sync_target_hash = sync_target.hash();
921
922 let mut header_downloader = ReverseHeadersDownloaderBuilder::default()
924 .build(Arc::new(client), Arc::new(TestConsensus::default()));
925 header_downloader.update_local_head(local_header.clone());
926 header_downloader.update_sync_target(SyncTarget::Tip(sync_target_hash));
927
928 let mut downloaded_headers_chunk = header_downloader.next().await.unwrap().unwrap();
930
931 local_header = sync_target;
933
934 downloaded_headers_chunk.reverse();
936 downloaded_headers.extend_from_slice(&downloaded_headers_chunk);
937 }
938
939 assert_eq!(headers[1..], downloaded_headers);
941 }
942
943 #[tokio::test]
944 async fn test_chunk_download_headers_from_gzip_file() {
945 reth_tracing::init_test_tracing();
946
947 let (file, headers, _) = generate_bodies_file(0..=14).await;
949
950 let gzip_temp_file = tempfile::NamedTempFile::new().unwrap();
952 let gzip_path = gzip_temp_file.path().to_owned();
953 drop(gzip_temp_file); let mut original_file = file;
957 original_file.seek(SeekFrom::Start(0)).await.unwrap();
958 let mut original_content = Vec::new();
959 original_file.read_to_end(&mut original_content).await.unwrap();
960
961 let mut gzip_file = File::create(&gzip_path).await.unwrap();
962 let mut encoder = GzipEncoder::new(&mut gzip_file);
963
964 encoder.write_all(&original_content).await.unwrap();
966 encoder.shutdown().await.unwrap();
967 drop(gzip_file);
968
969 let gzip_file = File::open(&gzip_path).await.unwrap();
971
972 let chunk_byte_len = rand::rng().random_range(2000..=10_000);
975 trace!(target: "downloaders::file::test", chunk_byte_len);
976
977 let mut reader =
979 ChunkedFileReader::from_file(gzip_file, chunk_byte_len as u64, true).await.unwrap();
980
981 let mut downloaded_headers: Vec<SealedHeader> = vec![];
982
983 let mut local_header = headers.first().unwrap().clone();
984
985 while let Some(client) =
987 reader.next_chunk::<Block>(NoopConsensus::arc(), None).await.unwrap()
988 {
989 if client.headers_len() == 0 {
990 continue;
991 }
992
993 let sync_target = client.tip_header().expect("tip_header should not be None");
994
995 let sync_target_hash = sync_target.hash();
996
997 let mut header_downloader = ReverseHeadersDownloaderBuilder::default()
999 .build(Arc::new(client), Arc::new(TestConsensus::default()));
1000 header_downloader.update_local_head(local_header.clone());
1001 header_downloader.update_sync_target(SyncTarget::Tip(sync_target_hash));
1002
1003 let mut downloaded_headers_chunk = header_downloader.next().await.unwrap().unwrap();
1005
1006 local_header = sync_target;
1008
1009 downloaded_headers_chunk.reverse();
1011 downloaded_headers.extend_from_slice(&downloaded_headers_chunk);
1012 }
1013
1014 assert_eq!(headers[1..], downloaded_headers);
1016 }
1017}