1use std::{
6 collections::{BTreeSet, HashMap},
7 mem,
8 path::{Path, PathBuf},
9 sync::Arc,
10};
11
12use indexmap::IndexMap;
13use tokio::sync::watch;
14use zebra_chain::{
15 block::{self, Block, Hash, Height},
16 parameters::Network,
17 sprout::{self},
18 transparent,
19};
20
21use crate::{
22 constants::{MAX_INVALIDATED_BLOCKS, MAX_NON_FINALIZED_CHAIN_FORKS},
23 error::ReconsiderError,
24 request::{ContextuallyVerifiedBlock, FinalizableBlock},
25 service::{
26 check,
27 finalized_state::{calculate_deferred_pool_balance_change, ZebraDb},
28 InvalidateError,
29 },
30 SemanticallyVerifiedBlock, ValidateContextError, WatchReceiver,
31};
32
33mod backup;
34mod chain;
35
36#[cfg(test)]
37pub(crate) use backup::MIN_DURATION_BETWEEN_BACKUP_UPDATES;
38
39#[cfg(test)]
40mod tests;
41
42pub(crate) use chain::{Chain, SpendingTransactionId};
43
44pub struct NonFinalizedState {
52 chain_set: BTreeSet<Arc<Chain>>,
60
61 invalidated_blocks: IndexMap<Height, Arc<Vec<ContextuallyVerifiedBlock>>>,
64
65 pub network: Network,
69
70 should_count_metrics: bool,
79
80 #[cfg(feature = "progress-bar")]
82 chain_count_bar: Option<howudoin::Tx>,
83
84 #[cfg(feature = "progress-bar")]
89 chain_fork_length_bars: Vec<howudoin::Tx>,
90}
91
92impl std::fmt::Debug for NonFinalizedState {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 let mut f = f.debug_struct("NonFinalizedState");
95
96 f.field("chain_set", &self.chain_set)
97 .field("network", &self.network);
98
99 f.field("should_count_metrics", &self.should_count_metrics);
100
101 f.finish()
102 }
103}
104
105impl Clone for NonFinalizedState {
106 fn clone(&self) -> Self {
107 Self {
108 chain_set: self.chain_set.clone(),
109 network: self.network.clone(),
110 invalidated_blocks: self.invalidated_blocks.clone(),
111 should_count_metrics: self.should_count_metrics,
112 #[cfg(feature = "progress-bar")]
114 chain_count_bar: None,
115 #[cfg(feature = "progress-bar")]
116 chain_fork_length_bars: Vec::new(),
117 }
118 }
119}
120
121impl NonFinalizedState {
122 pub fn new(network: &Network) -> NonFinalizedState {
124 NonFinalizedState {
125 chain_set: Default::default(),
126 network: network.clone(),
127 invalidated_blocks: Default::default(),
128 should_count_metrics: true,
129 #[cfg(feature = "progress-bar")]
130 chain_count_bar: None,
131 #[cfg(feature = "progress-bar")]
132 chain_fork_length_bars: Vec::new(),
133 }
134 }
135
136 pub(crate) fn write_to_backup(&self, backup_dir_path: &Path) {
144 let backup_blocks: HashMap<block::Hash, PathBuf> =
145 backup::list_backup_dir_entries(backup_dir_path).collect();
146 backup::update_non_finalized_state_backup(backup_dir_path, self, backup_blocks);
147 }
148
149 pub async fn with_backup(
159 self,
160 backup_dir_path: Option<PathBuf>,
161 finalized_state: &ZebraDb,
162 should_restore_backup: bool,
163 skip_backup_task: bool,
164 ) -> (
165 Self,
166 watch::Sender<NonFinalizedState>,
167 WatchReceiver<NonFinalizedState>,
168 ) {
169 let with_watch_channel = |non_finalized_state: NonFinalizedState| {
170 let (sender, receiver) = watch::channel(non_finalized_state.clone());
171 (non_finalized_state, sender, WatchReceiver::new(receiver))
172 };
173
174 let Some(backup_dir_path) = backup_dir_path else {
175 return with_watch_channel(self);
176 };
177
178 if skip_backup_task {
179 tracing::info!(
180 ?backup_dir_path,
181 "restoring non-finalized blocks from backup (sync write mode, backup task skipped)"
182 );
183 } else {
184 tracing::info!(
185 ?backup_dir_path,
186 "restoring non-finalized blocks from backup and spawning backup task"
187 );
188 }
189
190 let non_finalized_state = {
191 let backup_dir_path = backup_dir_path.clone();
192 let finalized_state = finalized_state.clone();
193 tokio::task::spawn_blocking(move || {
194 std::fs::create_dir_all(&backup_dir_path)
196 .expect("failed to create non-finalized state backup directory");
197
198 if should_restore_backup {
199 backup::restore_backup(self, &backup_dir_path, &finalized_state)
200 } else {
201 self
202 }
203 })
204 .await
205 .expect("failed to join blocking task")
206 };
207
208 let (non_finalized_state, sender, receiver) = with_watch_channel(non_finalized_state);
209
210 if !skip_backup_task {
211 tokio::spawn(backup::run_backup_task(receiver.clone(), backup_dir_path));
212 }
213
214 if !non_finalized_state.is_chain_set_empty() {
215 let num_blocks_restored = non_finalized_state
216 .best_chain()
217 .expect("must have best chain if chain set is not empty")
218 .len();
219
220 tracing::info!(
221 ?num_blocks_restored,
222 "restored blocks from non-finalized backup cache"
223 );
224 }
225
226 (non_finalized_state, sender, receiver)
227 }
228
229 #[cfg(any(test, feature = "proptest-impl"))]
240 #[allow(dead_code)]
241 pub fn eq_internal_state(&self, other: &NonFinalizedState) -> bool {
242 self.chain_set.len() == other.chain_set.len()
246 && self
247 .chain_set
248 .iter()
249 .zip(other.chain_set.iter())
250 .all(|(self_chain, other_chain)| self_chain.eq_internal_state(other_chain))
251 && self.network == other.network
252 }
253
254 pub fn chain_iter(&self) -> impl Iterator<Item = &Arc<Chain>> {
258 self.chain_set.iter().rev()
259 }
260
261 fn insert_with<F>(&mut self, chain: Arc<Chain>, chain_filter: F)
264 where
265 F: FnOnce(&mut BTreeSet<Arc<Chain>>),
266 {
267 self.chain_set.insert(chain);
268
269 chain_filter(&mut self.chain_set);
270
271 while self.chain_set.len() > MAX_NON_FINALIZED_CHAIN_FORKS {
272 self.chain_set.pop_first();
274 }
275
276 self.update_metrics_bars();
277 }
278
279 fn insert(&mut self, chain: Arc<Chain>) {
281 self.insert_with(chain, |_ignored_chain| { })
282 }
283
284 pub fn finalize(&mut self) -> FinalizableBlock {
287 #[allow(clippy::mutable_key_type)]
291 let chains = mem::take(&mut self.chain_set);
292 let mut chains = chains.into_iter();
293
294 let mut best_chain = chains.next_back().expect("there's at least one chain");
296
297 let mut_best_chain = Arc::make_mut(&mut best_chain);
299
300 let side_chains = chains;
302
303 let (best_chain_root, root_treestate) = mut_best_chain.pop_root();
306
307 if !best_chain.is_empty() {
309 self.insert(best_chain);
310 }
311
312 for mut side_chain in side_chains.rev() {
314 if side_chain.non_finalized_root_hash() != best_chain_root.hash {
315 drop(side_chain);
318
319 continue;
320 }
321
322 let mut_side_chain = Arc::make_mut(&mut side_chain);
326
327 let (side_chain_root, _treestate) = mut_side_chain.pop_root();
329 assert_eq!(side_chain_root.hash, best_chain_root.hash);
330
331 if !side_chain.is_empty() {
333 self.insert(side_chain);
334 }
335 }
336
337 self.invalidated_blocks
339 .retain(|height, _blocks| *height >= best_chain_root.height);
340
341 self.update_metrics_for_chains();
342
343 FinalizableBlock::new(best_chain_root, root_treestate)
345 }
346
347 #[tracing::instrument(level = "debug", skip(self, finalized_state, prepared))]
351 pub fn commit_block(
352 &mut self,
353 prepared: SemanticallyVerifiedBlock,
354 finalized_state: &ZebraDb,
355 ) -> Result<(), ValidateContextError> {
356 let parent_hash = prepared.block.header.previous_block_hash;
357 let (height, hash) = (prepared.height, prepared.hash);
358
359 let parent_chain = self.parent_chain(parent_hash)?;
360
361 let modified_chain = self.validate_and_commit(parent_chain, prepared, finalized_state)?;
364
365 self.insert_with(modified_chain, |chain_set| {
370 chain_set.retain(|chain| chain.non_finalized_tip_hash() != parent_hash)
371 });
372
373 self.update_metrics_for_committed_block(height, hash);
374
375 Ok(())
376 }
377
378 #[allow(clippy::unwrap_in_result)]
381 pub fn invalidate_block(&mut self, block_hash: Hash) -> Result<block::Hash, InvalidateError> {
382 let Some(chain) = self.find_chain(|chain| chain.contains_block_hash(block_hash)) else {
383 return Err(InvalidateError::BlockNotFound(block_hash));
384 };
385
386 let invalidated_blocks = if chain.non_finalized_root_hash() == block_hash {
387 self.chain_set.remove(&chain);
388 chain.blocks.values().cloned().collect()
389 } else {
390 let (new_chain, invalidated_blocks) = chain
391 .invalidate_block(block_hash)
392 .expect("already checked that chain contains hash");
393
394 self.insert_with(Arc::new(new_chain.clone()), |chain_set| {
397 chain_set.retain(|c| !c.contains_block_hash(block_hash))
398 });
399
400 invalidated_blocks
401 };
402
403 self.invalidated_blocks.insert(
405 invalidated_blocks
406 .first()
407 .expect("should not be empty")
408 .clone()
409 .height,
410 Arc::new(invalidated_blocks),
411 );
412
413 while self.invalidated_blocks.len() > MAX_INVALIDATED_BLOCKS {
414 self.invalidated_blocks.shift_remove_index(0);
415 }
416
417 self.update_metrics_for_chains();
418 self.update_metrics_bars();
419
420 Ok(block_hash)
421 }
422
423 #[allow(clippy::unwrap_in_result)]
427 pub fn reconsider_block(
428 &mut self,
429 block_hash: block::Hash,
430 finalized_state: &ZebraDb,
431 ) -> Result<Vec<block::Hash>, ReconsiderError> {
432 let (height, invalidated_blocks) = self
436 .invalidated_blocks
437 .iter()
438 .find_map(|(height, blocks)| {
439 if blocks.first()?.hash == block_hash {
440 Some((*height, (**blocks).clone()))
441 } else {
442 None
443 }
444 })
445 .ok_or(ReconsiderError::MissingInvalidatedBlock(block_hash))?;
446
447 let invalidated_block_hashes = invalidated_blocks
448 .iter()
449 .map(|block| block.hash)
450 .collect::<Vec<_>>();
451
452 let invalidated_root = invalidated_blocks
455 .first()
456 .ok_or(ReconsiderError::InvalidatedBlocksEmpty)?;
457
458 let root_parent_hash = invalidated_root.block.header.previous_block_hash;
459
460 let chain_result = if root_parent_hash == finalized_state.finalized_tip_hash() {
463 let chain = Chain::new(
464 &self.network,
465 finalized_state
466 .finalized_tip_height()
467 .ok_or(ReconsiderError::ParentChainNotFound(block_hash))?,
468 finalized_state.note_commitment_trees_for_tip(),
469 finalized_state.history_tree(),
470 finalized_state.finalized_value_pool(),
471 );
472 Arc::new(chain)
473 } else {
474 self.parent_chain(root_parent_hash)
477 .map_err(|_| ReconsiderError::ParentChainNotFound(block_hash))?
478 };
479
480 let mut modified_chain = Arc::unwrap_or_clone(chain_result);
481 for block in invalidated_blocks {
482 modified_chain = modified_chain
483 .push(block)
484 .map_err(ReconsiderError::ReplayFailed)?;
485 }
486
487 let (tip_height, tip_hash) = modified_chain.non_finalized_tip();
488
489 self.invalidated_blocks.shift_remove(&height);
493
494 if let Some(best_chain_root_height) = finalized_state.finalized_tip_height() {
497 self.invalidated_blocks
498 .retain(|height, _blocks| *height >= best_chain_root_height);
499 }
500
501 self.insert_with(Arc::new(modified_chain), |chain_set| {
502 chain_set.retain(|chain| chain.non_finalized_tip_hash() != root_parent_hash)
503 });
504
505 self.update_metrics_for_committed_block(tip_height, tip_hash);
506
507 Ok(invalidated_block_hashes)
508 }
509
510 #[tracing::instrument(level = "debug", skip(self, finalized_state, prepared))]
513 #[allow(clippy::unwrap_in_result)]
514 pub fn commit_new_chain(
515 &mut self,
516 prepared: SemanticallyVerifiedBlock,
517 finalized_state: &ZebraDb,
518 ) -> Result<(), ValidateContextError> {
519 let finalized_tip_height = finalized_state.finalized_tip_height();
520
521 #[cfg(not(test))]
523 let finalized_tip_height = finalized_tip_height.expect("finalized state contains blocks");
524 #[cfg(test)]
525 let finalized_tip_height = finalized_tip_height.unwrap_or(zebra_chain::block::Height(0));
526
527 let chain = Chain::new(
528 &self.network,
529 finalized_tip_height,
530 finalized_state.note_commitment_trees_for_tip(),
531 finalized_state.history_tree(),
532 finalized_state.finalized_value_pool(),
533 );
534
535 let (height, hash) = (prepared.height, prepared.hash);
536
537 let chain = self.validate_and_commit(Arc::new(chain), prepared, finalized_state)?;
539
540 self.insert(chain);
542 self.update_metrics_for_committed_block(height, hash);
543
544 Ok(())
545 }
546
547 #[tracing::instrument(level = "debug", skip(self, finalized_state, new_chain))]
553 fn validate_and_commit(
554 &self,
555 new_chain: Arc<Chain>,
556 prepared: SemanticallyVerifiedBlock,
557 finalized_state: &ZebraDb,
558 ) -> Result<Arc<Chain>, ValidateContextError> {
559 if self
560 .invalidated_blocks
561 .values()
562 .any(|blocks| blocks.iter().any(|block| block.hash == prepared.hash))
563 {
564 return Err(ValidateContextError::BlockPreviouslyInvalidated {
565 block_hash: prepared.hash,
566 });
567 }
568
569 let spent_utxos = check::utxo::transparent_spend(
573 &prepared,
574 &new_chain.unspent_utxos(),
575 &new_chain.spent_utxos,
576 finalized_state,
577 )?;
578
579 check::anchors::block_sapling_orchard_anchors_refer_to_final_treestates(
581 finalized_state,
582 &new_chain,
583 &prepared,
584 )?;
585
586 let sprout_final_treestates = check::anchors::block_fetch_sprout_final_treestates(
588 finalized_state,
589 &new_chain,
590 &prepared,
591 );
592
593 let contextual = ContextuallyVerifiedBlock::with_block_and_spent_utxos(
595 prepared.clone(),
596 spent_utxos.clone(),
597 calculate_deferred_pool_balance_change(prepared.height, &self.network),
598 )
599 .map_err(|value_balance_error| {
600 ValidateContextError::CalculateBlockChainValueChange {
601 value_balance_error,
602 height: prepared.height,
603 block_hash: prepared.hash,
604 transaction_count: prepared.block.transactions.len(),
605 spent_utxo_count: spent_utxos.len(),
606 }
607 })?;
608
609 Self::validate_and_update_parallel(new_chain, contextual, sprout_final_treestates)
610 }
611
612 #[allow(clippy::unwrap_in_result)]
614 #[tracing::instrument(skip(new_chain, sprout_final_treestates))]
615 fn validate_and_update_parallel(
616 new_chain: Arc<Chain>,
617 contextual: ContextuallyVerifiedBlock,
618 sprout_final_treestates: HashMap<sprout::tree::Root, Arc<sprout::tree::NoteCommitmentTree>>,
619 ) -> Result<Arc<Chain>, ValidateContextError> {
620 let mut block_commitment_result = None;
621 let mut sprout_anchor_result = None;
622 let mut chain_push_result = None;
623
624 let block = contextual.block.clone();
626 let network = new_chain.network();
627 let history_tree = new_chain.history_block_commitment_tree();
628
629 let block2 = contextual.block.clone();
630 let height = contextual.height;
631 let transaction_hashes = contextual.transaction_hashes.clone();
632
633 rayon::in_place_scope_fifo(|scope| {
634 scope.spawn_fifo(|_scope| {
635 block_commitment_result = Some(check::block_commitment_is_valid_for_chain_history(
636 block,
637 &network,
638 &history_tree,
639 ));
640 });
641
642 scope.spawn_fifo(|_scope| {
643 sprout_anchor_result =
644 Some(check::anchors::block_sprout_anchors_refer_to_treestates(
645 sprout_final_treestates,
646 block2,
647 transaction_hashes,
648 height,
649 ));
650 });
651
652 scope.spawn_fifo(|_scope| {
658 let new_chain = Arc::try_unwrap(new_chain)
661 .unwrap_or_else(|shared_chain| (*shared_chain).clone());
662 chain_push_result = Some(new_chain.push(contextual).map(Arc::new));
663 });
664 });
665
666 block_commitment_result.expect("scope has finished")?;
668 sprout_anchor_result.expect("scope has finished")?;
669
670 chain_push_result.expect("scope has finished")
671 }
672
673 pub fn best_chain_len(&self) -> Option<u32> {
676 Some(self.best_chain()?.blocks.len() as u32)
680 }
681
682 pub fn root_height(&self) -> Option<block::Height> {
684 self.best_chain()
685 .map(|chain| chain.non_finalized_root_height())
686 }
687
688 #[allow(dead_code)]
691 pub fn any_chain_contains(&self, hash: &block::Hash) -> bool {
692 self.chain_set
693 .iter()
694 .rev()
695 .any(|chain| chain.height_by_hash.contains_key(hash))
696 }
697
698 pub fn find_chain<P>(&self, mut predicate: P) -> Option<Arc<Chain>>
703 where
704 P: FnMut(&Chain) -> bool,
705 {
706 self.chain_set
708 .iter()
709 .rev()
710 .find(|chain| predicate(chain))
711 .cloned()
712 }
713
714 pub fn any_utxo(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Utxo> {
719 self.chain_set
720 .iter()
721 .rev()
722 .find_map(|chain| chain.created_utxo(outpoint))
723 }
724
725 #[allow(dead_code)]
727 pub fn any_block_by_hash(&self, hash: block::Hash) -> Option<Arc<Block>> {
728 for chain in self.chain_set.iter().rev() {
730 if let Some(prepared) = chain
731 .height_by_hash
732 .get(&hash)
733 .and_then(|height| chain.blocks.get(height))
734 {
735 return Some(prepared.block.clone());
736 }
737 }
738
739 None
740 }
741
742 #[allow(dead_code)]
744 pub fn any_prev_block_hash_for_hash(&self, hash: block::Hash) -> Option<block::Hash> {
745 self.any_block_by_hash(hash)
747 .map(|block| block.header.previous_block_hash)
748 }
749
750 #[allow(dead_code)]
752 pub fn best_hash(&self, height: block::Height) -> Option<block::Hash> {
753 self.best_chain()?
754 .blocks
755 .get(&height)
756 .map(|prepared| prepared.hash)
757 }
758
759 #[allow(dead_code)]
761 pub fn best_tip(&self) -> Option<(block::Height, block::Hash)> {
762 let best_chain = self.best_chain()?;
763 let height = best_chain.non_finalized_tip_height();
764 let hash = best_chain.non_finalized_tip_hash();
765
766 Some((height, hash))
767 }
768
769 #[allow(dead_code)]
771 pub fn best_tip_block(&self) -> Option<&ContextuallyVerifiedBlock> {
772 let best_chain = self.best_chain()?;
773
774 best_chain.tip_block()
775 }
776
777 #[allow(dead_code)]
779 pub fn best_height_by_hash(&self, hash: block::Hash) -> Option<block::Height> {
780 let best_chain = self.best_chain()?;
781 let height = *best_chain.height_by_hash.get(&hash)?;
782 Some(height)
783 }
784
785 #[allow(dead_code)]
787 pub fn any_height_by_hash(&self, hash: block::Hash) -> Option<block::Height> {
788 for chain in self.chain_set.iter().rev() {
789 if let Some(height) = chain.height_by_hash.get(&hash) {
790 return Some(*height);
791 }
792 }
793
794 None
795 }
796
797 #[cfg(any(test, feature = "proptest-impl"))]
799 #[allow(dead_code)]
800 pub fn best_contains_sprout_nullifier(&self, sprout_nullifier: &sprout::Nullifier) -> bool {
801 self.best_chain()
802 .map(|best_chain| best_chain.sprout_nullifiers.contains_key(sprout_nullifier))
803 .unwrap_or(false)
804 }
805
806 #[cfg(any(test, feature = "proptest-impl"))]
808 #[allow(dead_code)]
809 pub fn best_contains_sapling_nullifier(
810 &self,
811 sapling_nullifier: &zebra_chain::sapling::Nullifier,
812 ) -> bool {
813 self.best_chain()
814 .map(|best_chain| {
815 best_chain
816 .sapling_nullifiers
817 .contains_key(sapling_nullifier)
818 })
819 .unwrap_or(false)
820 }
821
822 #[cfg(any(test, feature = "proptest-impl"))]
824 #[allow(dead_code)]
825 pub fn best_contains_orchard_nullifier(
826 &self,
827 orchard_nullifier: &zebra_chain::orchard::Nullifier,
828 ) -> bool {
829 self.best_chain()
830 .map(|best_chain| {
831 best_chain
832 .orchard_nullifiers
833 .contains_key(orchard_nullifier)
834 })
835 .unwrap_or(false)
836 }
837
838 pub fn best_chain(&self) -> Option<&Arc<Chain>> {
840 self.chain_iter().next()
841 }
842
843 pub fn chain_count(&self) -> usize {
845 self.chain_set.len()
846 }
847
848 pub fn is_chain_set_empty(&self) -> bool {
850 self.chain_count() == 0
851 }
852
853 pub fn invalidated_blocks(&self) -> IndexMap<Height, Arc<Vec<ContextuallyVerifiedBlock>>> {
855 self.invalidated_blocks.clone()
856 }
857
858 fn parent_chain(&self, parent_hash: block::Hash) -> Result<Arc<Chain>, ValidateContextError> {
863 match self.find_chain(|chain| chain.non_finalized_tip_hash() == parent_hash) {
864 Some(chain) => Ok(chain.clone()),
866 None => {
868 let fork_chain = self
871 .chain_set
872 .iter()
873 .rev()
874 .find_map(|chain| chain.fork(parent_hash))
875 .ok_or(ValidateContextError::NotReadyToBeCommitted)?;
876
877 Ok(Arc::new(fork_chain))
878 }
879 }
880 }
881
882 fn should_count_metrics(&self) -> bool {
884 self.should_count_metrics
885 }
886
887 fn update_metrics_for_committed_block(&self, height: block::Height, hash: block::Hash) {
889 if !self.should_count_metrics() {
890 return;
891 }
892
893 metrics::counter!("state.memory.committed.block.count").increment(1);
894 metrics::gauge!("state.memory.committed.block.height").set(height.0 as f64);
895
896 if self
897 .best_chain()
898 .expect("metrics are only updated after initialization")
899 .non_finalized_tip_hash()
900 == hash
901 {
902 metrics::counter!("state.memory.best.committed.block.count").increment(1);
903 metrics::gauge!("state.memory.best.committed.block.height").set(height.0 as f64);
904 }
905
906 self.update_metrics_for_chains();
907 }
908
909 fn update_metrics_for_chains(&self) {
911 if !self.should_count_metrics() {
912 return;
913 }
914
915 metrics::gauge!("state.memory.chain.count").set(self.chain_set.len() as f64);
916 metrics::gauge!("state.memory.best.chain.length",)
917 .set(self.best_chain_len().unwrap_or_default() as f64);
918 }
919
920 fn update_metrics_bars(&mut self) {
923 if !self.should_count_metrics() {
926 #[allow(clippy::needless_return)]
927 return;
928 }
929
930 #[cfg(feature = "progress-bar")]
931 {
932 use std::cmp::Ordering::*;
933
934 if matches!(howudoin::cancelled(), Some(true)) {
935 self.disable_metrics();
936 return;
937 }
938
939 if self.chain_count_bar.is_none() {
941 self.chain_count_bar = Some(howudoin::new_root().label("Chain Forks"));
942 }
943
944 let chain_count_bar = self
945 .chain_count_bar
946 .as_ref()
947 .expect("just initialized if missing");
948 let finalized_tip_height = self
949 .best_chain()
950 .map(|chain| chain.non_finalized_root_height().0 - 1);
951
952 chain_count_bar.set_pos(u64::try_from(self.chain_count()).expect("fits in u64"));
953 if let Some(finalized_tip_height) = finalized_tip_height {
956 chain_count_bar.desc(format!("Finalized Root {finalized_tip_height}"));
957 }
958
959 let prev_length_bars = self.chain_fork_length_bars.len();
961
962 match self.chain_count().cmp(&prev_length_bars) {
963 Greater => self
964 .chain_fork_length_bars
965 .resize_with(self.chain_count(), || {
966 howudoin::new_with_parent(chain_count_bar.id())
967 }),
968 Less => {
969 let redundant_bars = self.chain_fork_length_bars.split_off(self.chain_count());
970 for bar in redundant_bars {
971 bar.close();
972 }
973 }
974 Equal => {}
975 }
976
977 for (chain_length_bar, chain) in
980 std::iter::zip(self.chain_fork_length_bars.iter(), self.chain_iter())
981 {
982 let fork_height = chain
983 .last_fork_height
984 .unwrap_or_else(|| chain.non_finalized_tip_height())
985 .0;
986
987 chain_length_bar
991 .label(format!("Fork {fork_height}"))
992 .set_pos(u64::try_from(chain.len()).expect("fits in u64"));
993 let mut desc = String::new();
1003
1004 if let Some(recent_fork_height) = chain.recent_fork_height() {
1005 let recent_fork_length = chain
1006 .recent_fork_length()
1007 .expect("just checked recent fork height");
1008
1009 let mut plural = "s";
1010 if recent_fork_length == 1 {
1011 plural = "";
1012 }
1013
1014 desc.push_str(&format!(
1015 " at {recent_fork_height:?} + {recent_fork_length} block{plural}"
1016 ));
1017 }
1018
1019 chain_length_bar.desc(desc);
1020 }
1021 }
1022 }
1023
1024 pub fn disable_metrics(&mut self) {
1026 self.should_count_metrics = false;
1027
1028 #[cfg(feature = "progress-bar")]
1029 {
1030 let count_bar = self.chain_count_bar.take().into_iter();
1031 let fork_bars = self.chain_fork_length_bars.drain(..);
1032 count_bar.chain(fork_bars).for_each(howudoin::Tx::close);
1033 }
1034 }
1035}
1036
1037impl Drop for NonFinalizedState {
1038 fn drop(&mut self) {
1039 self.disable_metrics();
1040 }
1041}