1use std::{
6 collections::{BTreeSet, HashMap},
7 mem,
8 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::{check, finalized_state::ZebraDb, InvalidateError},
26 SemanticallyVerifiedBlock, ValidateContextError, WatchReceiver,
27};
28
29mod backup;
30mod chain;
31
32#[cfg(test)]
33pub(crate) use backup::MIN_DURATION_BETWEEN_BACKUP_UPDATES;
34
35#[cfg(test)]
36mod tests;
37
38pub(crate) use chain::{Chain, SpendingTransactionId};
39
40pub struct NonFinalizedState {
48 chain_set: BTreeSet<Arc<Chain>>,
56
57 invalidated_blocks: IndexMap<Height, Arc<Vec<ContextuallyVerifiedBlock>>>,
60
61 pub network: Network,
65
66 should_count_metrics: bool,
75
76 #[cfg(feature = "progress-bar")]
78 chain_count_bar: Option<howudoin::Tx>,
79
80 #[cfg(feature = "progress-bar")]
85 chain_fork_length_bars: Vec<howudoin::Tx>,
86}
87
88impl std::fmt::Debug for NonFinalizedState {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 let mut f = f.debug_struct("NonFinalizedState");
91
92 f.field("chain_set", &self.chain_set)
93 .field("network", &self.network);
94
95 f.field("should_count_metrics", &self.should_count_metrics);
96
97 f.finish()
98 }
99}
100
101impl Clone for NonFinalizedState {
102 fn clone(&self) -> Self {
103 Self {
104 chain_set: self.chain_set.clone(),
105 network: self.network.clone(),
106 invalidated_blocks: self.invalidated_blocks.clone(),
107 should_count_metrics: self.should_count_metrics,
108 #[cfg(feature = "progress-bar")]
110 chain_count_bar: None,
111 #[cfg(feature = "progress-bar")]
112 chain_fork_length_bars: Vec::new(),
113 }
114 }
115}
116
117impl NonFinalizedState {
118 pub fn new(network: &Network) -> NonFinalizedState {
120 NonFinalizedState {
121 chain_set: Default::default(),
122 network: network.clone(),
123 invalidated_blocks: Default::default(),
124 should_count_metrics: true,
125 #[cfg(feature = "progress-bar")]
126 chain_count_bar: None,
127 #[cfg(feature = "progress-bar")]
128 chain_fork_length_bars: Vec::new(),
129 }
130 }
131
132 pub async fn with_backup(
142 self,
143 backup_dir_path: Option<PathBuf>,
144 finalized_state: &ZebraDb,
145 should_restore_backup: bool,
146 ) -> (
147 Self,
148 watch::Sender<NonFinalizedState>,
149 WatchReceiver<NonFinalizedState>,
150 ) {
151 let with_watch_channel = |non_finalized_state: NonFinalizedState| {
152 let (sender, receiver) = watch::channel(non_finalized_state.clone());
153 (non_finalized_state, sender, WatchReceiver::new(receiver))
154 };
155
156 let Some(backup_dir_path) = backup_dir_path else {
157 return with_watch_channel(self);
158 };
159
160 tracing::info!(
161 ?backup_dir_path,
162 "restoring non-finalized blocks from backup and spawning backup task"
163 );
164
165 let non_finalized_state = {
166 let backup_dir_path = backup_dir_path.clone();
167 let finalized_state = finalized_state.clone();
168 tokio::task::spawn_blocking(move || {
169 std::fs::create_dir_all(&backup_dir_path)
171 .expect("failed to create non-finalized state backup directory");
172
173 if should_restore_backup {
174 backup::restore_backup(self, &backup_dir_path, &finalized_state)
175 } else {
176 self
177 }
178 })
179 .await
180 .expect("failed to join blocking task")
181 };
182
183 let (non_finalized_state, sender, receiver) = with_watch_channel(non_finalized_state);
184
185 tokio::spawn(backup::run_backup_task(receiver.clone(), backup_dir_path));
186
187 if !non_finalized_state.is_chain_set_empty() {
188 let num_blocks_restored = non_finalized_state
189 .best_chain()
190 .expect("must have best chain if chain set is not empty")
191 .len();
192
193 tracing::info!(
194 ?num_blocks_restored,
195 "restored blocks from non-finalized backup cache"
196 );
197 }
198
199 (non_finalized_state, sender, receiver)
200 }
201
202 #[cfg(any(test, feature = "proptest-impl"))]
213 #[allow(dead_code)]
214 pub fn eq_internal_state(&self, other: &NonFinalizedState) -> bool {
215 self.chain_set.len() == other.chain_set.len()
219 && self
220 .chain_set
221 .iter()
222 .zip(other.chain_set.iter())
223 .all(|(self_chain, other_chain)| self_chain.eq_internal_state(other_chain))
224 && self.network == other.network
225 }
226
227 pub fn chain_iter(&self) -> impl Iterator<Item = &Arc<Chain>> {
231 self.chain_set.iter().rev()
232 }
233
234 fn insert_with<F>(&mut self, chain: Arc<Chain>, chain_filter: F)
237 where
238 F: FnOnce(&mut BTreeSet<Arc<Chain>>),
239 {
240 self.chain_set.insert(chain);
241
242 chain_filter(&mut self.chain_set);
243
244 while self.chain_set.len() > MAX_NON_FINALIZED_CHAIN_FORKS {
245 self.chain_set.pop_first();
247 }
248
249 self.update_metrics_bars();
250 }
251
252 fn insert(&mut self, chain: Arc<Chain>) {
254 self.insert_with(chain, |_ignored_chain| { })
255 }
256
257 pub fn finalize(&mut self) -> FinalizableBlock {
260 #[allow(clippy::mutable_key_type)]
264 let chains = mem::take(&mut self.chain_set);
265 let mut chains = chains.into_iter();
266
267 let mut best_chain = chains.next_back().expect("there's at least one chain");
269
270 let mut_best_chain = Arc::make_mut(&mut best_chain);
272
273 let side_chains = chains;
275
276 let (best_chain_root, root_treestate) = mut_best_chain.pop_root();
279
280 if !best_chain.is_empty() {
282 self.insert(best_chain);
283 }
284
285 for mut side_chain in side_chains.rev() {
287 if side_chain.non_finalized_root_hash() != best_chain_root.hash {
288 drop(side_chain);
291
292 continue;
293 }
294
295 let mut_side_chain = Arc::make_mut(&mut side_chain);
299
300 let (side_chain_root, _treestate) = mut_side_chain.pop_root();
302 assert_eq!(side_chain_root.hash, best_chain_root.hash);
303
304 self.insert(side_chain);
306 }
307
308 self.invalidated_blocks
310 .retain(|height, _blocks| *height >= best_chain_root.height);
311
312 self.update_metrics_for_chains();
313
314 FinalizableBlock::new(best_chain_root, root_treestate)
316 }
317
318 #[tracing::instrument(level = "debug", skip(self, finalized_state, prepared))]
322 pub fn commit_block(
323 &mut self,
324 prepared: SemanticallyVerifiedBlock,
325 finalized_state: &ZebraDb,
326 ) -> Result<(), ValidateContextError> {
327 let parent_hash = prepared.block.header.previous_block_hash;
328 let (height, hash) = (prepared.height, prepared.hash);
329
330 let parent_chain = self.parent_chain(parent_hash)?;
331
332 let modified_chain = self.validate_and_commit(parent_chain, prepared, finalized_state)?;
335
336 self.insert_with(modified_chain, |chain_set| {
341 chain_set.retain(|chain| chain.non_finalized_tip_hash() != parent_hash)
342 });
343
344 self.update_metrics_for_committed_block(height, hash);
345
346 Ok(())
347 }
348
349 #[allow(clippy::unwrap_in_result)]
352 pub fn invalidate_block(&mut self, block_hash: Hash) -> Result<block::Hash, InvalidateError> {
353 let Some(chain) = self.find_chain(|chain| chain.contains_block_hash(block_hash)) else {
354 return Err(InvalidateError::BlockNotFound(block_hash));
355 };
356
357 let invalidated_blocks = if chain.non_finalized_root_hash() == block_hash {
358 self.chain_set.remove(&chain);
359 chain.blocks.values().cloned().collect()
360 } else {
361 let (new_chain, invalidated_blocks) = chain
362 .invalidate_block(block_hash)
363 .expect("already checked that chain contains hash");
364
365 self.insert_with(Arc::new(new_chain.clone()), |chain_set| {
368 chain_set.retain(|c| !c.contains_block_hash(block_hash))
369 });
370
371 invalidated_blocks
372 };
373
374 self.invalidated_blocks.insert(
376 invalidated_blocks
377 .first()
378 .expect("should not be empty")
379 .clone()
380 .height,
381 Arc::new(invalidated_blocks),
382 );
383
384 while self.invalidated_blocks.len() > MAX_INVALIDATED_BLOCKS {
385 self.invalidated_blocks.shift_remove_index(0);
386 }
387
388 self.update_metrics_for_chains();
389 self.update_metrics_bars();
390
391 Ok(block_hash)
392 }
393
394 #[allow(clippy::unwrap_in_result)]
398 pub fn reconsider_block(
399 &mut self,
400 block_hash: block::Hash,
401 finalized_state: &ZebraDb,
402 ) -> Result<Vec<block::Hash>, ReconsiderError> {
403 let height = self
405 .invalidated_blocks
406 .iter()
407 .find_map(|(height, blocks)| {
408 if blocks.first()?.hash == block_hash {
409 Some(height)
410 } else {
411 None
412 }
413 })
414 .ok_or(ReconsiderError::MissingInvalidatedBlock(block_hash))?;
415
416 let invalidated_blocks = Arc::unwrap_or_clone(
417 self.invalidated_blocks
418 .clone()
419 .shift_remove(height)
420 .ok_or(ReconsiderError::MissingInvalidatedBlock(block_hash))?,
421 );
422
423 let invalidated_block_hashes = invalidated_blocks
424 .iter()
425 .map(|block| block.hash)
426 .collect::<Vec<_>>();
427
428 let invalidated_root = invalidated_blocks
431 .first()
432 .ok_or(ReconsiderError::InvalidatedBlocksEmpty)?;
433
434 let root_parent_hash = invalidated_root.block.header.previous_block_hash;
435
436 let chain_result = if root_parent_hash == finalized_state.finalized_tip_hash() {
439 let chain = Chain::new(
440 &self.network,
441 finalized_state
442 .finalized_tip_height()
443 .ok_or(ReconsiderError::ParentChainNotFound(block_hash))?,
444 finalized_state.sprout_tree_for_tip(),
445 finalized_state.sapling_tree_for_tip(),
446 finalized_state.orchard_tree_for_tip(),
447 finalized_state.history_tree(),
448 finalized_state.finalized_value_pool(),
449 );
450 Arc::new(chain)
451 } else {
452 self.parent_chain(root_parent_hash)
455 .map_err(|_| ReconsiderError::ParentChainNotFound(block_hash))?
456 };
457
458 let mut modified_chain = Arc::unwrap_or_clone(chain_result);
459 for block in invalidated_blocks {
460 modified_chain = modified_chain
461 .push(block)
462 .expect("previously invalidated block should be valid for chain");
463 }
464
465 let (height, hash) = modified_chain.non_finalized_tip();
466
467 if let Some(best_chain_root_height) = finalized_state.finalized_tip_height() {
470 self.invalidated_blocks
471 .retain(|height, _blocks| *height >= best_chain_root_height);
472 }
473
474 self.insert_with(Arc::new(modified_chain), |chain_set| {
475 chain_set.retain(|chain| chain.non_finalized_tip_hash() != root_parent_hash)
476 });
477
478 self.update_metrics_for_committed_block(height, hash);
479
480 Ok(invalidated_block_hashes)
481 }
482
483 #[tracing::instrument(level = "debug", skip(self, finalized_state, prepared))]
486 #[allow(clippy::unwrap_in_result)]
487 pub fn commit_new_chain(
488 &mut self,
489 prepared: SemanticallyVerifiedBlock,
490 finalized_state: &ZebraDb,
491 ) -> Result<(), ValidateContextError> {
492 let finalized_tip_height = finalized_state.finalized_tip_height();
493
494 #[cfg(not(test))]
496 let finalized_tip_height = finalized_tip_height.expect("finalized state contains blocks");
497 #[cfg(test)]
498 let finalized_tip_height = finalized_tip_height.unwrap_or(zebra_chain::block::Height(0));
499
500 let chain = Chain::new(
501 &self.network,
502 finalized_tip_height,
503 finalized_state.sprout_tree_for_tip(),
504 finalized_state.sapling_tree_for_tip(),
505 finalized_state.orchard_tree_for_tip(),
506 finalized_state.history_tree(),
507 finalized_state.finalized_value_pool(),
508 );
509
510 let (height, hash) = (prepared.height, prepared.hash);
511
512 let chain = self.validate_and_commit(Arc::new(chain), prepared, finalized_state)?;
514
515 self.insert(chain);
517 self.update_metrics_for_committed_block(height, hash);
518
519 Ok(())
520 }
521
522 #[tracing::instrument(level = "debug", skip(self, finalized_state, new_chain))]
528 fn validate_and_commit(
529 &self,
530 new_chain: Arc<Chain>,
531 prepared: SemanticallyVerifiedBlock,
532 finalized_state: &ZebraDb,
533 ) -> Result<Arc<Chain>, ValidateContextError> {
534 if self
535 .invalidated_blocks
536 .values()
537 .any(|blocks| blocks.iter().any(|block| block.hash == prepared.hash))
538 {
539 return Err(ValidateContextError::BlockPreviouslyInvalidated {
540 block_hash: prepared.hash,
541 });
542 }
543
544 let spent_utxos = check::utxo::transparent_spend(
548 &prepared,
549 &new_chain.unspent_utxos(),
550 &new_chain.spent_utxos,
551 finalized_state,
552 )?;
553
554 check::anchors::block_sapling_orchard_anchors_refer_to_final_treestates(
556 finalized_state,
557 &new_chain,
558 &prepared,
559 )?;
560
561 let sprout_final_treestates = check::anchors::block_fetch_sprout_final_treestates(
563 finalized_state,
564 &new_chain,
565 &prepared,
566 );
567
568 let contextual = ContextuallyVerifiedBlock::with_block_and_spent_utxos(
570 prepared.clone(),
571 spent_utxos.clone(),
572 )
573 .map_err(|value_balance_error| {
574 ValidateContextError::CalculateBlockChainValueChange {
575 value_balance_error,
576 height: prepared.height,
577 block_hash: prepared.hash,
578 transaction_count: prepared.block.transactions.len(),
579 spent_utxo_count: spent_utxos.len(),
580 }
581 })?;
582
583 Self::validate_and_update_parallel(new_chain, contextual, sprout_final_treestates)
584 }
585
586 #[allow(clippy::unwrap_in_result)]
588 #[tracing::instrument(skip(new_chain, sprout_final_treestates))]
589 fn validate_and_update_parallel(
590 new_chain: Arc<Chain>,
591 contextual: ContextuallyVerifiedBlock,
592 sprout_final_treestates: HashMap<sprout::tree::Root, Arc<sprout::tree::NoteCommitmentTree>>,
593 ) -> Result<Arc<Chain>, ValidateContextError> {
594 let mut block_commitment_result = None;
595 let mut sprout_anchor_result = None;
596 let mut chain_push_result = None;
597
598 let block = contextual.block.clone();
600 let network = new_chain.network();
601 let history_tree = new_chain.history_block_commitment_tree();
602
603 let block2 = contextual.block.clone();
604 let height = contextual.height;
605 let transaction_hashes = contextual.transaction_hashes.clone();
606
607 rayon::in_place_scope_fifo(|scope| {
608 scope.spawn_fifo(|_scope| {
609 block_commitment_result = Some(check::block_commitment_is_valid_for_chain_history(
610 block,
611 &network,
612 &history_tree,
613 ));
614 });
615
616 scope.spawn_fifo(|_scope| {
617 sprout_anchor_result =
618 Some(check::anchors::block_sprout_anchors_refer_to_treestates(
619 sprout_final_treestates,
620 block2,
621 transaction_hashes,
622 height,
623 ));
624 });
625
626 scope.spawn_fifo(|_scope| {
632 let new_chain = Arc::try_unwrap(new_chain)
635 .unwrap_or_else(|shared_chain| (*shared_chain).clone());
636 chain_push_result = Some(new_chain.push(contextual).map(Arc::new));
637 });
638 });
639
640 block_commitment_result.expect("scope has finished")?;
642 sprout_anchor_result.expect("scope has finished")?;
643
644 chain_push_result.expect("scope has finished")
645 }
646
647 pub fn best_chain_len(&self) -> Option<u32> {
650 Some(self.best_chain()?.blocks.len() as u32)
653 }
654
655 pub fn root_height(&self) -> Option<block::Height> {
657 self.best_chain()
658 .map(|chain| chain.non_finalized_root_height())
659 }
660
661 #[allow(dead_code)]
664 pub fn any_chain_contains(&self, hash: &block::Hash) -> bool {
665 self.chain_set
666 .iter()
667 .rev()
668 .any(|chain| chain.height_by_hash.contains_key(hash))
669 }
670
671 pub fn find_chain<P>(&self, mut predicate: P) -> Option<Arc<Chain>>
676 where
677 P: FnMut(&Chain) -> bool,
678 {
679 self.chain_set
681 .iter()
682 .rev()
683 .find(|chain| predicate(chain))
684 .cloned()
685 }
686
687 pub fn any_utxo(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Utxo> {
692 self.chain_set
693 .iter()
694 .rev()
695 .find_map(|chain| chain.created_utxo(outpoint))
696 }
697
698 #[allow(dead_code)]
700 pub fn any_block_by_hash(&self, hash: block::Hash) -> Option<Arc<Block>> {
701 for chain in self.chain_set.iter().rev() {
703 if let Some(prepared) = chain
704 .height_by_hash
705 .get(&hash)
706 .and_then(|height| chain.blocks.get(height))
707 {
708 return Some(prepared.block.clone());
709 }
710 }
711
712 None
713 }
714
715 #[allow(dead_code)]
717 pub fn any_prev_block_hash_for_hash(&self, hash: block::Hash) -> Option<block::Hash> {
718 self.any_block_by_hash(hash)
720 .map(|block| block.header.previous_block_hash)
721 }
722
723 #[allow(dead_code)]
725 pub fn best_hash(&self, height: block::Height) -> Option<block::Hash> {
726 self.best_chain()?
727 .blocks
728 .get(&height)
729 .map(|prepared| prepared.hash)
730 }
731
732 #[allow(dead_code)]
734 pub fn best_tip(&self) -> Option<(block::Height, block::Hash)> {
735 let best_chain = self.best_chain()?;
736 let height = best_chain.non_finalized_tip_height();
737 let hash = best_chain.non_finalized_tip_hash();
738
739 Some((height, hash))
740 }
741
742 #[allow(dead_code)]
744 pub fn best_tip_block(&self) -> Option<&ContextuallyVerifiedBlock> {
745 let best_chain = self.best_chain()?;
746
747 best_chain.tip_block()
748 }
749
750 #[allow(dead_code)]
752 pub fn best_height_by_hash(&self, hash: block::Hash) -> Option<block::Height> {
753 let best_chain = self.best_chain()?;
754 let height = *best_chain.height_by_hash.get(&hash)?;
755 Some(height)
756 }
757
758 #[allow(dead_code)]
760 pub fn any_height_by_hash(&self, hash: block::Hash) -> Option<block::Height> {
761 for chain in self.chain_set.iter().rev() {
762 if let Some(height) = chain.height_by_hash.get(&hash) {
763 return Some(*height);
764 }
765 }
766
767 None
768 }
769
770 #[cfg(any(test, feature = "proptest-impl"))]
772 #[allow(dead_code)]
773 pub fn best_contains_sprout_nullifier(&self, sprout_nullifier: &sprout::Nullifier) -> bool {
774 self.best_chain()
775 .map(|best_chain| best_chain.sprout_nullifiers.contains_key(sprout_nullifier))
776 .unwrap_or(false)
777 }
778
779 #[cfg(any(test, feature = "proptest-impl"))]
781 #[allow(dead_code)]
782 pub fn best_contains_sapling_nullifier(
783 &self,
784 sapling_nullifier: &zebra_chain::sapling::Nullifier,
785 ) -> bool {
786 self.best_chain()
787 .map(|best_chain| {
788 best_chain
789 .sapling_nullifiers
790 .contains_key(sapling_nullifier)
791 })
792 .unwrap_or(false)
793 }
794
795 #[cfg(any(test, feature = "proptest-impl"))]
797 #[allow(dead_code)]
798 pub fn best_contains_orchard_nullifier(
799 &self,
800 orchard_nullifier: &zebra_chain::orchard::Nullifier,
801 ) -> bool {
802 self.best_chain()
803 .map(|best_chain| {
804 best_chain
805 .orchard_nullifiers
806 .contains_key(orchard_nullifier)
807 })
808 .unwrap_or(false)
809 }
810
811 pub fn best_chain(&self) -> Option<&Arc<Chain>> {
813 self.chain_iter().next()
814 }
815
816 pub fn chain_count(&self) -> usize {
818 self.chain_set.len()
819 }
820
821 pub fn is_chain_set_empty(&self) -> bool {
823 self.chain_count() == 0
824 }
825
826 pub fn invalidated_blocks(&self) -> IndexMap<Height, Arc<Vec<ContextuallyVerifiedBlock>>> {
828 self.invalidated_blocks.clone()
829 }
830
831 fn parent_chain(&self, parent_hash: block::Hash) -> Result<Arc<Chain>, ValidateContextError> {
836 match self.find_chain(|chain| chain.non_finalized_tip_hash() == parent_hash) {
837 Some(chain) => Ok(chain.clone()),
839 None => {
841 let fork_chain = self
844 .chain_set
845 .iter()
846 .rev()
847 .find_map(|chain| chain.fork(parent_hash))
848 .ok_or(ValidateContextError::NotReadyToBeCommitted)?;
849
850 Ok(Arc::new(fork_chain))
851 }
852 }
853 }
854
855 fn should_count_metrics(&self) -> bool {
857 self.should_count_metrics
858 }
859
860 fn update_metrics_for_committed_block(&self, height: block::Height, hash: block::Hash) {
862 if !self.should_count_metrics() {
863 return;
864 }
865
866 metrics::counter!("state.memory.committed.block.count").increment(1);
867 metrics::gauge!("state.memory.committed.block.height").set(height.0 as f64);
868
869 if self
870 .best_chain()
871 .expect("metrics are only updated after initialization")
872 .non_finalized_tip_hash()
873 == hash
874 {
875 metrics::counter!("state.memory.best.committed.block.count").increment(1);
876 metrics::gauge!("state.memory.best.committed.block.height").set(height.0 as f64);
877 }
878
879 self.update_metrics_for_chains();
880 }
881
882 fn update_metrics_for_chains(&self) {
884 if !self.should_count_metrics() {
885 return;
886 }
887
888 metrics::gauge!("state.memory.chain.count").set(self.chain_set.len() as f64);
889 metrics::gauge!("state.memory.best.chain.length",)
890 .set(self.best_chain_len().unwrap_or_default() as f64);
891 }
892
893 fn update_metrics_bars(&mut self) {
896 if !self.should_count_metrics() {
899 #[allow(clippy::needless_return)]
900 return;
901 }
902
903 #[cfg(feature = "progress-bar")]
904 {
905 use std::cmp::Ordering::*;
906
907 if matches!(howudoin::cancelled(), Some(true)) {
908 self.disable_metrics();
909 return;
910 }
911
912 if self.chain_count_bar.is_none() {
914 self.chain_count_bar = Some(howudoin::new_root().label("Chain Forks"));
915 }
916
917 let chain_count_bar = self
918 .chain_count_bar
919 .as_ref()
920 .expect("just initialized if missing");
921 let finalized_tip_height = self
922 .best_chain()
923 .map(|chain| chain.non_finalized_root_height().0 - 1);
924
925 chain_count_bar.set_pos(u64::try_from(self.chain_count()).expect("fits in u64"));
926 if let Some(finalized_tip_height) = finalized_tip_height {
929 chain_count_bar.desc(format!("Finalized Root {finalized_tip_height}"));
930 }
931
932 let prev_length_bars = self.chain_fork_length_bars.len();
934
935 match self.chain_count().cmp(&prev_length_bars) {
936 Greater => self
937 .chain_fork_length_bars
938 .resize_with(self.chain_count(), || {
939 howudoin::new_with_parent(chain_count_bar.id())
940 }),
941 Less => {
942 let redundant_bars = self.chain_fork_length_bars.split_off(self.chain_count());
943 for bar in redundant_bars {
944 bar.close();
945 }
946 }
947 Equal => {}
948 }
949
950 for (chain_length_bar, chain) in
953 std::iter::zip(self.chain_fork_length_bars.iter(), self.chain_iter())
954 {
955 let fork_height = chain
956 .last_fork_height
957 .unwrap_or_else(|| chain.non_finalized_tip_height())
958 .0;
959
960 chain_length_bar
964 .label(format!("Fork {fork_height}"))
965 .set_pos(u64::try_from(chain.len()).expect("fits in u64"));
966 let mut desc = String::new();
976
977 if let Some(recent_fork_height) = chain.recent_fork_height() {
978 let recent_fork_length = chain
979 .recent_fork_length()
980 .expect("just checked recent fork height");
981
982 let mut plural = "s";
983 if recent_fork_length == 1 {
984 plural = "";
985 }
986
987 desc.push_str(&format!(
988 " at {recent_fork_height:?} + {recent_fork_length} block{plural}"
989 ));
990 }
991
992 chain_length_bar.desc(desc);
993 }
994 }
995 }
996
997 pub fn disable_metrics(&mut self) {
999 self.should_count_metrics = false;
1000
1001 #[cfg(feature = "progress-bar")]
1002 {
1003 let count_bar = self.chain_count_bar.take().into_iter();
1004 let fork_bars = self.chain_fork_length_bars.drain(..);
1005 count_bar.chain(fork_bars).for_each(howudoin::Tx::close);
1006 }
1007 }
1008}
1009
1010impl Drop for NonFinalizedState {
1011 fn drop(&mut self) {
1012 self.disable_metrics();
1013 }
1014}