1use alloy_consensus::BlockHeader;
2use alloy_primitives::{BlockHash, BlockNumber, Bytes, B256};
3use alloy_rlp::Decodable;
4use futures_util::StreamExt;
5use reth_config::config::EtlConfig;
6use reth_db_api::{
7 cursor::{DbCursorRO, DbCursorRW},
8 table::Value,
9 tables,
10 transaction::{DbTx, DbTxMut},
11 DbTxUnwindExt, RawKey, RawTable, RawValue,
12};
13use reth_etl::Collector;
14use reth_network_p2p::headers::{
15 downloader::{HeaderDownloader, HeaderSyncGap, SyncTarget},
16 error::HeadersDownloaderError,
17};
18use reth_primitives_traits::{FullBlockHeader, HeaderTy, NodePrimitives, SealedHeader};
19use reth_provider::{
20 providers::StaticFileWriter, BlockHashReader, DBProvider, HeaderSyncGapProvider,
21 StaticFileProviderFactory,
22};
23use reth_stages_api::{
24 CheckpointBlockRange, EntitiesCheckpoint, ExecInput, ExecOutput, HeadersCheckpoint, Stage,
25 StageCheckpoint, StageError, StageId, UnwindInput, UnwindOutput,
26};
27use reth_static_file_types::StaticFileSegment;
28use std::task::{ready, Context, Poll};
29
30use tokio::sync::watch;
31use tracing::*;
32
33#[derive(Debug)]
45pub struct HeaderStage<Provider, Downloader: HeaderDownloader> {
46 provider: Provider,
48 downloader: Downloader,
50 tip: watch::Receiver<B256>,
54 sync_gap: Option<HeaderSyncGap<Downloader::Header>>,
56 hash_collector: Collector<BlockHash, BlockNumber>,
58 header_collector: Collector<BlockNumber, Bytes>,
60 is_etl_ready: bool,
62}
63
64impl<Provider, Downloader> HeaderStage<Provider, Downloader>
67where
68 Downloader: HeaderDownloader,
69{
70 pub fn new(
72 database: Provider,
73 downloader: Downloader,
74 tip: watch::Receiver<B256>,
75 etl_config: EtlConfig,
76 ) -> Self {
77 Self {
78 provider: database,
79 downloader,
80 tip,
81 sync_gap: None,
82 hash_collector: Collector::new(etl_config.file_size / 2, etl_config.dir.clone()),
83 header_collector: Collector::new(etl_config.file_size / 2, etl_config.dir),
84 is_etl_ready: false,
85 }
86 }
87
88 fn clear_etl_state(&mut self) {
90 self.sync_gap = None;
91 self.hash_collector.clear();
92 self.header_collector.clear();
93 self.is_etl_ready = false;
94 }
95
96 fn write_headers<P>(&mut self, provider: &P) -> Result<BlockNumber, StageError>
101 where
102 P: DBProvider<Tx: DbTxMut> + StaticFileProviderFactory,
103 Downloader: HeaderDownloader<Header = <P::Primitives as NodePrimitives>::BlockHeader>,
104 <P::Primitives as NodePrimitives>::BlockHeader: Value + FullBlockHeader,
105 {
106 let total_headers = self.header_collector.len();
107
108 info!(target: "sync::stages::headers", total = total_headers, "Writing headers");
109
110 let static_file_provider = provider.static_file_provider();
111
112 let mut last_header_number = static_file_provider
115 .get_highest_static_file_block(StaticFileSegment::Headers)
116 .unwrap_or_default();
117
118 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers)?;
121 let interval = (total_headers / 10).max(1);
122 for (index, header) in self.header_collector.iter()?.enumerate() {
123 let (_, header_buf) = header?;
124
125 if index > 0 && index.is_multiple_of(interval) && total_headers > 100 {
126 info!(target: "sync::stages::headers", progress = %format_args!("{:.2}%", (index as f64 / total_headers as f64) * 100.0), "Writing headers");
127 }
128
129 let sealed_header: SealedHeader<Downloader::Header> = SealedHeader::new_unhashed(
130 Decodable::decode(&mut header_buf.as_slice())
131 .map_err(|err| StageError::Fatal(Box::new(err)))?,
132 );
133
134 let (header, header_hash) = sealed_header.split_ref();
135 if header.number() == 0 {
136 continue
137 }
138 last_header_number = header.number();
139
140 writer.append_header(header, header_hash)?;
142 }
143
144 info!(target: "sync::stages::headers", total = total_headers, "Writing headers hash index");
145
146 let mut cursor_header_numbers =
147 provider.tx_ref().cursor_write::<RawTable<tables::HeaderNumbers>>()?;
148 let first_sync = if provider.tx_ref().entries::<RawTable<tables::HeaderNumbers>>()? == 1 &&
151 let Some((hash, block_number)) = cursor_header_numbers.last()? &&
152 block_number.value()? == 0
153 {
154 self.hash_collector.insert(hash.key()?, 0)?;
155 cursor_header_numbers.delete_current()?;
156 true
157 } else {
158 false
159 };
160
161 for (index, hash_to_number) in self.hash_collector.iter()?.enumerate() {
164 let (hash, number) = hash_to_number?;
165
166 if index > 0 && index.is_multiple_of(interval) && total_headers > 100 {
167 info!(target: "sync::stages::headers", progress = %format_args!("{:.2}%", (index as f64 / total_headers as f64) * 100.0), "Writing headers hash index");
168 }
169
170 if first_sync {
171 cursor_header_numbers.append(
172 RawKey::<BlockHash>::from_vec(hash),
173 &RawValue::<BlockNumber>::from_vec(number),
174 )?;
175 } else {
176 cursor_header_numbers.upsert(
177 RawKey::<BlockHash>::from_vec(hash),
178 &RawValue::<BlockNumber>::from_vec(number),
179 )?;
180 }
181 }
182
183 Ok(last_header_number)
184 }
185}
186
187impl<Provider, P, D> Stage<Provider> for HeaderStage<P, D>
188where
189 Provider: DBProvider<Tx: DbTxMut> + StaticFileProviderFactory,
190 P: HeaderSyncGapProvider<Header = <Provider::Primitives as NodePrimitives>::BlockHeader>,
191 D: HeaderDownloader<Header = <Provider::Primitives as NodePrimitives>::BlockHeader>,
192 <Provider::Primitives as NodePrimitives>::BlockHeader: FullBlockHeader + Value,
193{
194 fn id(&self) -> StageId {
196 StageId::Headers
197 }
198
199 fn poll_execute_ready(
200 &mut self,
201 cx: &mut Context<'_>,
202 input: ExecInput,
203 ) -> Poll<Result<(), StageError>> {
204 let current_checkpoint = input.checkpoint();
205
206 if self.is_etl_ready {
208 return Poll::Ready(Ok(()))
209 }
210
211 let local_head = self.provider.local_tip_header(current_checkpoint.block_number)?;
213 let target = SyncTarget::Tip(*self.tip.borrow());
214 let gap = HeaderSyncGap { local_head, target };
215 let tip = gap.target.tip();
216
217 if gap.is_closed() {
219 info!(
220 target: "sync::stages::headers",
221 checkpoint = %current_checkpoint.block_number,
222 target = ?tip,
223 "Target block already reached"
224 );
225 self.is_etl_ready = true;
226 self.sync_gap = Some(gap);
227 return Poll::Ready(Ok(()))
228 }
229
230 debug!(target: "sync::stages::headers", ?tip, head = ?gap.local_head.hash(), "Commencing sync");
231 let local_head_number = gap.local_head.number();
232
233 if self.sync_gap != Some(gap.clone()) {
235 self.sync_gap = Some(gap.clone());
236 self.downloader.update_sync_gap(gap.local_head, gap.target);
237 }
238
239 loop {
241 match ready!(self.downloader.poll_next_unpin(cx)) {
242 Some(Ok(headers)) => {
243 info!(target: "sync::stages::headers", total = headers.len(), from_block = headers.first().map(|h| h.number()), to_block = headers.last().map(|h| h.number()), "Received headers");
244 for header in headers {
245 let header_number = header.number();
246
247 self.hash_collector.insert(header.hash(), header_number)?;
248 self.header_collector
249 .insert(header_number, Bytes::from(alloy_rlp::encode(&*header)))?;
250
251 if header_number == local_head_number + 1 {
254 self.is_etl_ready = true;
255 return Poll::Ready(Ok(()))
256 }
257 }
258 }
259 Some(Err(HeadersDownloaderError::DetachedHead { local_head, header, error })) => {
260 error!(target: "sync::stages::headers", %error, "Cannot attach header to head");
261 self.clear_etl_state();
262 return Poll::Ready(Err(StageError::DetachedHead {
263 local_head: Box::new(local_head.block_with_parent()),
264 header: Box::new(header.block_with_parent()),
265 error,
266 }))
267 }
268 None => {
269 self.clear_etl_state();
270 return Poll::Ready(Err(StageError::ChannelClosed))
271 }
272 }
273 }
274 }
275
276 fn execute(&mut self, provider: &Provider, input: ExecInput) -> Result<ExecOutput, StageError> {
279 let current_checkpoint = input.checkpoint();
280
281 if self.sync_gap.take().ok_or(StageError::MissingSyncGap)?.is_closed() {
282 self.is_etl_ready = false;
283 return Ok(ExecOutput::done(current_checkpoint))
284 }
285
286 if !self.is_etl_ready {
288 return Err(StageError::MissingDownloadBuffer)
289 }
290
291 self.is_etl_ready = false;
293
294 let to_be_processed = self.hash_collector.len() as u64;
296 let last_header_number = self.write_headers(provider)?;
297
298 self.hash_collector.clear();
300 self.header_collector.clear();
301
302 Ok(ExecOutput::done(
305 StageCheckpoint::new(last_header_number).with_headers_stage_checkpoint(
306 HeadersCheckpoint {
307 block_range: CheckpointBlockRange {
308 from: input.checkpoint().block_number,
309 to: last_header_number,
310 },
311 progress: EntitiesCheckpoint {
312 processed: input.checkpoint().block_number + to_be_processed,
313 total: last_header_number,
314 },
315 },
316 ),
317 ))
318 }
319
320 fn unwind(
322 &mut self,
323 provider: &Provider,
324 input: UnwindInput,
325 ) -> Result<UnwindOutput, StageError> {
326 self.clear_etl_state();
327
328 provider
332 .tx_ref()
333 .unwind_table_by_walker::<tables::CanonicalHeaders, tables::HeaderNumbers>(
334 (input.unwind_to + 1)..,
335 )?;
336 provider.tx_ref().unwind_table_by_num::<tables::CanonicalHeaders>(input.unwind_to)?;
337 let unfinalized_headers_unwound = provider.tx_ref().unwind_table_by_num::<tables::Headers<
338 HeaderTy<Provider::Primitives>,
339 >>(input.unwind_to)?;
340
341 let static_file_provider = provider.static_file_provider();
344 let highest_block = static_file_provider
345 .get_highest_static_file_block(StaticFileSegment::Headers)
346 .unwrap_or_default();
347 let static_file_headers_to_unwind = highest_block - input.unwind_to;
348 for block_number in (input.unwind_to + 1)..=highest_block {
349 let hash = static_file_provider.block_hash(block_number)?;
350 if let Some(header_hash) = hash {
356 provider.tx_ref().delete::<tables::HeaderNumbers>(header_hash, None)?;
357 }
358 }
359
360 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers)?;
362 writer.prune_headers(static_file_headers_to_unwind)?;
363
364 let stage_checkpoint =
367 input.checkpoint.headers_stage_checkpoint().map(|stage_checkpoint| HeadersCheckpoint {
368 block_range: stage_checkpoint.block_range,
369 progress: EntitiesCheckpoint {
370 processed: stage_checkpoint.progress.processed.saturating_sub(
371 static_file_headers_to_unwind + unfinalized_headers_unwound as u64,
372 ),
373 total: stage_checkpoint.progress.total,
374 },
375 });
376
377 let mut checkpoint = StageCheckpoint::new(input.unwind_to);
378 if let Some(stage_checkpoint) = stage_checkpoint {
379 checkpoint = checkpoint.with_headers_stage_checkpoint(stage_checkpoint);
380 }
381
382 Ok(UnwindOutput { checkpoint })
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use crate::test_utils::{
390 stage_test_suite, ExecuteStageTestRunner, StageTestRunner, UnwindStageTestRunner,
391 };
392 use alloy_primitives::B256;
393 use assert_matches::assert_matches;
394 use reth_provider::{test_utils::insert_headers, ProviderFactory, StaticFileProviderFactory};
395 use reth_stages_api::StageUnitCheckpoint;
396 use reth_testing_utils::generators::{self, random_header, random_header_range};
397 use std::sync::Arc;
398 use test_runner::HeadersTestRunner;
399
400 mod test_runner {
401 use super::*;
402 use crate::test_utils::{TestRunnerError, TestStageDB};
403 use reth_consensus::test_utils::TestConsensus;
404 use reth_downloaders::headers::reverse_headers::{
405 ReverseHeadersDownloader, ReverseHeadersDownloaderBuilder,
406 };
407 use reth_network_p2p::test_utils::{TestHeaderDownloader, TestHeadersClient};
408 use reth_provider::{test_utils::MockNodeTypesWithDB, BlockNumReader, HeaderProvider};
409 use tokio::sync::watch;
410
411 pub(crate) struct HeadersTestRunner<D: HeaderDownloader> {
412 pub(crate) client: TestHeadersClient,
413 channel: (watch::Sender<B256>, watch::Receiver<B256>),
414 downloader_factory: Box<dyn Fn() -> D + Send + Sync + 'static>,
415 db: TestStageDB,
416 }
417
418 impl Default for HeadersTestRunner<TestHeaderDownloader> {
419 fn default() -> Self {
420 let client = TestHeadersClient::default();
421 Self {
422 client: client.clone(),
423 channel: watch::channel(B256::ZERO),
424
425 downloader_factory: Box::new(move || {
426 TestHeaderDownloader::new(client.clone(), 1000, 1000)
427 }),
428 db: TestStageDB::default(),
429 }
430 }
431 }
432
433 impl<D: HeaderDownloader<Header = alloy_consensus::Header> + 'static> StageTestRunner
434 for HeadersTestRunner<D>
435 {
436 type S = HeaderStage<ProviderFactory<MockNodeTypesWithDB>, D>;
437
438 fn db(&self) -> &TestStageDB {
439 &self.db
440 }
441
442 fn stage(&self) -> Self::S {
443 HeaderStage::new(
444 self.db.factory.clone(),
445 (*self.downloader_factory)(),
446 self.channel.1.clone(),
447 EtlConfig::default(),
448 )
449 }
450 }
451
452 impl<D: HeaderDownloader<Header = alloy_consensus::Header> + 'static> ExecuteStageTestRunner
453 for HeadersTestRunner<D>
454 {
455 type Seed = Vec<SealedHeader>;
456
457 fn seed_execution(&mut self, input: ExecInput) -> Result<Self::Seed, TestRunnerError> {
458 let mut rng = generators::rng();
459 let start = input.checkpoint().block_number;
460 let headers = random_header_range(&mut rng, 0..start + 1, B256::ZERO);
461 let head = headers.last().cloned().unwrap();
462 self.db.insert_headers(headers.iter())?;
463
464 let end = input.target.unwrap_or_default() + 1;
466
467 if start + 1 >= end {
468 return Ok(Vec::default())
469 }
470
471 let mut headers = random_header_range(&mut rng, start + 1..end, head.hash());
472 headers.insert(0, head);
473 Ok(headers)
474 }
475
476 fn validate_execution(
478 &self,
479 input: ExecInput,
480 output: Option<ExecOutput>,
481 ) -> Result<(), TestRunnerError> {
482 let initial_checkpoint = input.checkpoint().block_number;
483 match output {
484 Some(output) if output.checkpoint.block_number > initial_checkpoint => {
485 let provider = self.db.factory.provider()?;
486
487 for block_num in initial_checkpoint..output.checkpoint.block_number {
488 let hash = provider.block_hash(block_num)?.expect("no header hash");
490
491 assert_eq!(provider.block_number(hash)?, Some(block_num));
493
494 let header = provider.header_by_number(block_num)?;
496 assert!(header.is_some());
497 let header = SealedHeader::seal_slow(header.unwrap());
498 assert_eq!(header.hash(), hash);
499 }
500 }
501 _ => self.check_no_header_entry_above(initial_checkpoint)?,
502 };
503 Ok(())
504 }
505
506 async fn after_execution(&self, headers: Self::Seed) -> Result<(), TestRunnerError> {
507 self.client.extend(headers.iter().map(|h| h.clone_header())).await;
508 let tip = if headers.is_empty() {
509 let tip = random_header(&mut generators::rng(), 0, None);
510 self.db.insert_headers(std::iter::once(&tip))?;
511 tip.hash()
512 } else {
513 headers.last().unwrap().hash()
514 };
515 self.send_tip(tip);
516 Ok(())
517 }
518 }
519
520 impl<D: HeaderDownloader<Header = alloy_consensus::Header> + 'static> UnwindStageTestRunner
521 for HeadersTestRunner<D>
522 {
523 fn validate_unwind(&self, input: UnwindInput) -> Result<(), TestRunnerError> {
524 self.check_no_header_entry_above(input.unwind_to)
525 }
526 }
527
528 impl HeadersTestRunner<ReverseHeadersDownloader<TestHeadersClient>> {
529 pub(crate) fn with_linear_downloader() -> Self {
530 let client = TestHeadersClient::default();
531 Self {
532 client: client.clone(),
533 channel: watch::channel(B256::ZERO),
534 downloader_factory: Box::new(move || {
535 ReverseHeadersDownloaderBuilder::default()
536 .stream_batch_size(500)
537 .build(client.clone(), Arc::new(TestConsensus::default()))
538 }),
539 db: TestStageDB::default(),
540 }
541 }
542 }
543
544 impl<D: HeaderDownloader> HeadersTestRunner<D> {
545 pub(crate) fn check_no_header_entry_above(
546 &self,
547 block: BlockNumber,
548 ) -> Result<(), TestRunnerError> {
549 self.db
550 .ensure_no_entry_above_by_value::<tables::HeaderNumbers, _>(block, |val| val)?;
551 self.db.ensure_no_entry_above::<tables::CanonicalHeaders, _>(block, |key| key)?;
552 self.db.ensure_no_entry_above::<tables::Headers, _>(block, |key| key)?;
553 Ok(())
554 }
555
556 pub(crate) fn send_tip(&self, tip: B256) {
557 self.channel.0.send(tip).expect("failed to send tip");
558 }
559 }
560 }
561
562 stage_test_suite!(HeadersTestRunner, headers);
563
564 #[tokio::test]
567 async fn execute_with_linear_downloader_unwind() {
568 let mut runner = HeadersTestRunner::with_linear_downloader();
569 let (checkpoint, previous_stage) = (1000, 1200);
570 let input = ExecInput {
571 target: Some(previous_stage),
572 checkpoint: Some(StageCheckpoint::new(checkpoint)),
573 };
574 let headers = runner.seed_execution(input).expect("failed to seed execution");
575 let rx = runner.execute(input);
576
577 runner.client.extend(headers.iter().rev().map(|h| h.clone_header())).await;
578
579 let tip = headers.last().unwrap();
581 runner.send_tip(tip.hash());
582
583 let result = rx.await.unwrap();
584 runner.db().factory.static_file_provider().commit().unwrap();
585 assert_matches!(result, Ok(ExecOutput { checkpoint: StageCheckpoint {
586 block_number,
587 stage_checkpoint: Some(StageUnitCheckpoint::Headers(HeadersCheckpoint {
588 block_range: CheckpointBlockRange {
589 from,
590 to
591 },
592 progress: EntitiesCheckpoint {
593 processed,
594 total,
595 }
596 }))
597 }, done: true }) if block_number == tip.number &&
598 from == checkpoint && to == previous_stage &&
599 processed == checkpoint + headers.len() as u64 - 1 && total == tip.number
601 );
602 assert!(runner.validate_execution(input, result.ok()).is_ok(), "validation failed");
603 assert!(runner.stage().hash_collector.is_empty());
604 assert!(runner.stage().header_collector.is_empty());
605
606 let sealed_headers = random_header_range(
608 &mut generators::rng(),
609 tip.number + 1..tip.number + 10,
610 tip.hash(),
611 );
612
613 insert_headers(&runner.db().factory, &sealed_headers);
614
615 let unwind_input = UnwindInput {
617 checkpoint: StageCheckpoint::new(tip.number + 10),
618 unwind_to: tip.number,
619 bad_block: None,
620 };
621
622 let unwind_output = runner.unwind(unwind_input).await.unwrap();
623 assert_eq!(unwind_output.checkpoint.block_number, tip.number);
624
625 assert!(runner.validate_unwind(unwind_input).is_ok());
627 }
628
629 #[tokio::test]
631 async fn execute_with_linear_downloader() {
632 let mut runner = HeadersTestRunner::with_linear_downloader();
633 let (checkpoint, previous_stage) = (1000, 1200);
634 let input = ExecInput {
635 target: Some(previous_stage),
636 checkpoint: Some(StageCheckpoint::new(checkpoint)),
637 };
638 let headers = runner.seed_execution(input).expect("failed to seed execution");
639 let rx = runner.execute(input);
640
641 runner.client.extend(headers.iter().rev().map(|h| h.clone_header())).await;
642
643 let tip = headers.last().unwrap();
645 runner.send_tip(tip.hash());
646
647 let result = rx.await.unwrap();
648 runner.db().factory.static_file_provider().commit().unwrap();
649 assert_matches!(result, Ok(ExecOutput { checkpoint: StageCheckpoint {
650 block_number,
651 stage_checkpoint: Some(StageUnitCheckpoint::Headers(HeadersCheckpoint {
652 block_range: CheckpointBlockRange {
653 from,
654 to
655 },
656 progress: EntitiesCheckpoint {
657 processed,
658 total,
659 }
660 }))
661 }, done: true }) if block_number == tip.number &&
662 from == checkpoint && to == previous_stage &&
663 processed == checkpoint + headers.len() as u64 - 1 && total == tip.number
665 );
666 assert!(runner.validate_execution(input, result.ok()).is_ok(), "validation failed");
667 assert!(runner.stage().hash_collector.is_empty());
668 assert!(runner.stage().header_collector.is_empty());
669 }
670}