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 !should_restore_backup {
179 tracing::info!(
180 ?backup_dir_path,
181 spawning_backup_task = !skip_backup_task,
182 "not restoring non-finalized blocks from backup, any backed up blocks that are \
183 missing from the non-finalized state will be deleted"
184 );
185 } else if skip_backup_task {
186 tracing::info!(
187 ?backup_dir_path,
188 "restoring non-finalized blocks from backup (sync write mode, backup task skipped)"
189 );
190 } else {
191 tracing::info!(
192 ?backup_dir_path,
193 "restoring non-finalized blocks from backup and spawning backup task"
194 );
195 }
196
197 let non_finalized_state = {
198 let backup_dir_path = backup_dir_path.clone();
199 let finalized_state = finalized_state.clone();
200 tokio::task::spawn_blocking(move || {
201 std::fs::create_dir_all(&backup_dir_path)
203 .expect("failed to create non-finalized state backup directory");
204
205 if should_restore_backup {
206 backup::restore_backup(self, &backup_dir_path, &finalized_state)
207 } else {
208 self
209 }
210 })
211 .await
212 .expect("failed to join blocking task")
213 };
214
215 let (non_finalized_state, sender, receiver) = with_watch_channel(non_finalized_state);
216
217 if !skip_backup_task {
218 tokio::spawn(backup::run_backup_task(receiver.clone(), backup_dir_path));
219 }
220
221 if !non_finalized_state.is_chain_set_empty() {
222 let num_blocks_restored = non_finalized_state
223 .best_chain()
224 .expect("must have best chain if chain set is not empty")
225 .len();
226
227 tracing::info!(
228 ?num_blocks_restored,
229 "restored blocks from non-finalized backup cache"
230 );
231 } else if should_restore_backup {
232 tracing::info!("no blocks were restored from the non-finalized backup cache");
233 }
234
235 (non_finalized_state, sender, receiver)
236 }
237
238 #[cfg(any(test, feature = "proptest-impl"))]
249 #[allow(dead_code)]
250 pub fn eq_internal_state(&self, other: &NonFinalizedState) -> bool {
251 self.chain_set.len() == other.chain_set.len()
255 && self
256 .chain_set
257 .iter()
258 .zip(other.chain_set.iter())
259 .all(|(self_chain, other_chain)| self_chain.eq_internal_state(other_chain))
260 && self.network == other.network
261 }
262
263 pub fn chain_iter(&self) -> impl Iterator<Item = &Arc<Chain>> {
267 self.chain_set.iter().rev()
268 }
269
270 fn insert_with<F>(&mut self, chain: Arc<Chain>, chain_filter: F)
273 where
274 F: FnOnce(&mut BTreeSet<Arc<Chain>>),
275 {
276 self.chain_set.insert(chain);
277
278 chain_filter(&mut self.chain_set);
279
280 while self.chain_set.len() > MAX_NON_FINALIZED_CHAIN_FORKS {
281 self.chain_set.pop_first();
283 }
284
285 self.update_metrics_bars();
286 }
287
288 fn insert(&mut self, chain: Arc<Chain>) {
290 self.insert_with(chain, |_ignored_chain| { })
291 }
292
293 pub fn finalize(&mut self) -> FinalizableBlock {
296 #[allow(clippy::mutable_key_type)]
300 let chains = mem::take(&mut self.chain_set);
301 let mut chains = chains.into_iter();
302
303 let mut best_chain = chains.next_back().expect("there's at least one chain");
305
306 let mut_best_chain = Arc::make_mut(&mut best_chain);
308
309 let side_chains = chains;
311
312 let (best_chain_root, root_treestate) = mut_best_chain.pop_root();
315
316 if !best_chain.is_empty() {
318 self.insert(best_chain);
319 }
320
321 for mut side_chain in side_chains.rev() {
323 if side_chain.non_finalized_root_hash() != best_chain_root.hash {
324 drop(side_chain);
327
328 continue;
329 }
330
331 let mut_side_chain = Arc::make_mut(&mut side_chain);
335
336 let (side_chain_root, _treestate) = mut_side_chain.pop_root();
338 assert_eq!(side_chain_root.hash, best_chain_root.hash);
339
340 if !side_chain.is_empty() {
342 self.insert(side_chain);
343 }
344 }
345
346 self.invalidated_blocks
348 .retain(|height, _blocks| *height >= best_chain_root.height);
349
350 self.update_metrics_for_chains();
351
352 FinalizableBlock::new(best_chain_root, root_treestate)
354 }
355
356 #[tracing::instrument(level = "debug", skip(self, finalized_state, prepared))]
360 pub fn commit_block(
361 &mut self,
362 prepared: SemanticallyVerifiedBlock,
363 finalized_state: &ZebraDb,
364 ) -> Result<(), ValidateContextError> {
365 let parent_hash = prepared.block.header.previous_block_hash;
366 let (height, hash) = (prepared.height, prepared.hash);
367
368 let parent_chain = self.parent_chain(parent_hash)?;
369
370 let modified_chain = self.validate_and_commit(parent_chain, prepared, finalized_state)?;
373
374 self.insert_with(modified_chain, |chain_set| {
379 chain_set.retain(|chain| chain.non_finalized_tip_hash() != parent_hash)
380 });
381
382 self.update_metrics_for_committed_block(height, hash);
383
384 Ok(())
385 }
386
387 #[allow(clippy::unwrap_in_result)]
392 pub fn invalidate_block(&mut self, block_hash: Hash) -> Result<block::Hash, InvalidateError> {
393 let Some(chain) = self.find_chain(|chain| chain.contains_block_hash(block_hash)) else {
394 return Err(InvalidateError::BlockNotFound(block_hash));
395 };
396
397 let invalidated_blocks = if chain.non_finalized_root_hash() == block_hash {
398 if self.chain_set.len() == 1 {
399 return Err(InvalidateError::OnlyChainRoot(block_hash));
400 }
401
402 self.chain_set.remove(&chain);
403 chain.blocks.values().cloned().collect()
404 } else {
405 let (new_chain, invalidated_blocks) = chain
406 .invalidate_block(block_hash)
407 .expect("already checked that chain contains hash");
408
409 self.insert_with(Arc::new(new_chain.clone()), |chain_set| {
412 chain_set.retain(|c| !c.contains_block_hash(block_hash))
413 });
414
415 invalidated_blocks
416 };
417
418 self.invalidated_blocks.insert(
420 invalidated_blocks
421 .first()
422 .expect("should not be empty")
423 .clone()
424 .height,
425 Arc::new(invalidated_blocks),
426 );
427
428 while self.invalidated_blocks.len() > MAX_INVALIDATED_BLOCKS {
429 self.invalidated_blocks.shift_remove_index(0);
430 }
431
432 self.update_metrics_for_chains();
433 self.update_metrics_bars();
434
435 Ok(block_hash)
436 }
437
438 #[allow(clippy::unwrap_in_result)]
442 pub fn reconsider_block(
443 &mut self,
444 block_hash: block::Hash,
445 finalized_state: &ZebraDb,
446 ) -> Result<Vec<block::Hash>, ReconsiderError> {
447 let (height, invalidated_blocks) = self
451 .invalidated_blocks
452 .iter()
453 .find_map(|(height, blocks)| {
454 if blocks.first()?.hash == block_hash {
455 Some((*height, (**blocks).clone()))
456 } else {
457 None
458 }
459 })
460 .ok_or(ReconsiderError::MissingInvalidatedBlock(block_hash))?;
461
462 let invalidated_block_hashes = invalidated_blocks
463 .iter()
464 .map(|block| block.hash)
465 .collect::<Vec<_>>();
466
467 let invalidated_root = invalidated_blocks
470 .first()
471 .ok_or(ReconsiderError::InvalidatedBlocksEmpty)?;
472
473 let root_parent_hash = invalidated_root.block.header.previous_block_hash;
474
475 let chain_result = if root_parent_hash == finalized_state.finalized_tip_hash() {
478 let chain = Chain::new(
479 &self.network,
480 finalized_state
481 .finalized_tip_height()
482 .ok_or(ReconsiderError::ParentChainNotFound(block_hash))?,
483 finalized_state.note_commitment_trees_for_tip(),
484 finalized_state.history_tree(),
485 finalized_state.finalized_value_pool(),
486 );
487 Arc::new(chain)
488 } else {
489 self.parent_chain(root_parent_hash)
492 .map_err(|_| ReconsiderError::ParentChainNotFound(block_hash))?
493 };
494
495 let mut modified_chain = Arc::unwrap_or_clone(chain_result);
496 for block in invalidated_blocks {
497 modified_chain = modified_chain
498 .push(block)
499 .map_err(ReconsiderError::ReplayFailed)?;
500 }
501
502 let (tip_height, tip_hash) = modified_chain.non_finalized_tip();
503
504 self.invalidated_blocks.shift_remove(&height);
508
509 if let Some(best_chain_root_height) = finalized_state.finalized_tip_height() {
512 self.invalidated_blocks
513 .retain(|height, _blocks| *height >= best_chain_root_height);
514 }
515
516 self.insert_with(Arc::new(modified_chain), |chain_set| {
517 chain_set.retain(|chain| chain.non_finalized_tip_hash() != root_parent_hash)
518 });
519
520 self.update_metrics_for_committed_block(tip_height, tip_hash);
521
522 Ok(invalidated_block_hashes)
523 }
524
525 #[tracing::instrument(level = "debug", skip(self, finalized_state, prepared))]
528 #[allow(clippy::unwrap_in_result)]
529 pub fn commit_new_chain(
530 &mut self,
531 prepared: SemanticallyVerifiedBlock,
532 finalized_state: &ZebraDb,
533 ) -> Result<(), ValidateContextError> {
534 let finalized_tip_height = finalized_state.finalized_tip_height();
535
536 #[cfg(not(test))]
538 let finalized_tip_height = finalized_tip_height.expect("finalized state contains blocks");
539 #[cfg(test)]
540 let finalized_tip_height = finalized_tip_height.unwrap_or(zebra_chain::block::Height(0));
541
542 let chain = Chain::new(
543 &self.network,
544 finalized_tip_height,
545 finalized_state.note_commitment_trees_for_tip(),
546 finalized_state.history_tree(),
547 finalized_state.finalized_value_pool(),
548 );
549
550 let (height, hash) = (prepared.height, prepared.hash);
551
552 let chain = self.validate_and_commit(Arc::new(chain), prepared, finalized_state)?;
554
555 self.insert(chain);
557 self.update_metrics_for_committed_block(height, hash);
558
559 Ok(())
560 }
561
562 #[tracing::instrument(level = "debug", skip(self, finalized_state, new_chain))]
568 fn validate_and_commit(
569 &self,
570 new_chain: Arc<Chain>,
571 prepared: SemanticallyVerifiedBlock,
572 finalized_state: &ZebraDb,
573 ) -> Result<Arc<Chain>, ValidateContextError> {
574 if self
575 .invalidated_blocks
576 .values()
577 .any(|blocks| blocks.iter().any(|block| block.hash == prepared.hash))
578 {
579 return Err(ValidateContextError::BlockPreviouslyInvalidated {
580 block_hash: prepared.hash,
581 });
582 }
583
584 let spent_utxos = check::utxo::transparent_spend(
588 &prepared,
589 &new_chain.unspent_utxos(),
590 &new_chain.spent_utxos,
591 finalized_state,
592 )?;
593
594 check::anchors::block_sapling_orchard_anchors_refer_to_final_treestates(
596 finalized_state,
597 &new_chain,
598 &prepared,
599 )?;
600
601 let sprout_final_treestates = check::anchors::block_fetch_sprout_final_treestates(
603 finalized_state,
604 &new_chain,
605 &prepared,
606 );
607
608 let (height, block_hash) = (prepared.height, prepared.hash);
609 let transaction_count = prepared.block.transactions.len();
610 let spent_utxo_count = spent_utxos.len();
611 let deferred_pool_balance_change = calculate_deferred_pool_balance_change(
612 height,
613 &self.network,
614 new_chain.chain_value_pools,
615 )
616 .map_err(|subsidy_error| ValidateContextError::InvalidSubsidy {
617 subsidy_error: subsidy_error.into(),
618 height,
619 block_hash,
620 })?;
621
622 let (contextual, block_fees) = ContextuallyVerifiedBlock::with_block_spent_utxos_and_fees(
623 prepared,
624 spent_utxos,
625 deferred_pool_balance_change,
626 &self.network,
627 new_chain.chain_value_pools,
628 )
629 .map_err(|value_balance_error| {
630 ValidateContextError::CalculateBlockChainValueChange {
631 value_balance_error,
632 height,
633 block_hash,
634 transaction_count,
635 spent_utxo_count,
636 }
637 })?;
638
639 if zebra_chain::parameters::subsidy::nsm_reissuance_is_active(height, &self.network) {
641 check::nsm_subsidy_is_valid(
642 &contextual,
643 &self.network,
644 new_chain.chain_value_pools,
645 block_fees.expect("reissuance starts no earlier than NU7 fee accounting"),
646 )?;
647 }
648
649 Self::validate_and_update_parallel(new_chain, contextual, sprout_final_treestates)
650 }
651
652 #[allow(clippy::unwrap_in_result)]
654 #[tracing::instrument(skip(new_chain, sprout_final_treestates))]
655 fn validate_and_update_parallel(
656 new_chain: Arc<Chain>,
657 contextual: ContextuallyVerifiedBlock,
658 sprout_final_treestates: HashMap<sprout::tree::Root, Arc<sprout::tree::NoteCommitmentTree>>,
659 ) -> Result<Arc<Chain>, ValidateContextError> {
660 let mut block_commitment_result = None;
661 let mut sprout_anchor_result = None;
662 let mut chain_push_result = None;
663
664 let block = contextual.block.clone();
666 let network = new_chain.network();
667 let history_tree = new_chain.history_block_commitment_tree();
668
669 let block2 = contextual.block.clone();
670 let height = contextual.height;
671 let transaction_hashes = contextual.transaction_hashes.clone();
672
673 rayon::in_place_scope_fifo(|scope| {
674 scope.spawn_fifo(|_scope| {
675 block_commitment_result = Some(check::block_commitment_is_valid_for_chain_history(
676 block,
677 &network,
678 &history_tree,
679 ));
680 });
681
682 scope.spawn_fifo(|_scope| {
683 sprout_anchor_result =
684 Some(check::anchors::block_sprout_anchors_refer_to_treestates(
685 sprout_final_treestates,
686 block2,
687 transaction_hashes,
688 height,
689 ));
690 });
691
692 scope.spawn_fifo(|_scope| {
698 let new_chain = Arc::try_unwrap(new_chain)
701 .unwrap_or_else(|shared_chain| (*shared_chain).clone());
702 chain_push_result = Some(new_chain.push(contextual).map(Arc::new));
703 });
704 });
705
706 block_commitment_result.expect("scope has finished")?;
708 sprout_anchor_result.expect("scope has finished")?;
709
710 chain_push_result.expect("scope has finished")
711 }
712
713 pub fn best_chain_len(&self) -> Option<u32> {
716 Some(self.best_chain()?.blocks.len() as u32)
720 }
721
722 pub fn root_height(&self) -> Option<block::Height> {
724 self.best_chain()
725 .map(|chain| chain.non_finalized_root_height())
726 }
727
728 #[allow(dead_code)]
731 pub fn any_chain_contains(&self, hash: &block::Hash) -> bool {
732 self.chain_set
733 .iter()
734 .rev()
735 .any(|chain| chain.height_by_hash.contains_key(hash))
736 }
737
738 pub fn find_chain<P>(&self, mut predicate: P) -> Option<Arc<Chain>>
743 where
744 P: FnMut(&Chain) -> bool,
745 {
746 self.chain_set
748 .iter()
749 .rev()
750 .find(|chain| predicate(chain))
751 .cloned()
752 }
753
754 pub fn any_utxo(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Utxo> {
759 self.chain_set
760 .iter()
761 .rev()
762 .find_map(|chain| chain.created_utxo(outpoint))
763 }
764
765 #[allow(dead_code)]
767 pub fn any_block_by_hash(&self, hash: block::Hash) -> Option<Arc<Block>> {
768 for chain in self.chain_set.iter().rev() {
770 if let Some(prepared) = chain
771 .height_by_hash
772 .get(&hash)
773 .and_then(|height| chain.blocks.get(height))
774 {
775 return Some(prepared.block.clone());
776 }
777 }
778
779 None
780 }
781
782 #[allow(dead_code)]
784 pub fn any_prev_block_hash_for_hash(&self, hash: block::Hash) -> Option<block::Hash> {
785 self.any_block_by_hash(hash)
787 .map(|block| block.header.previous_block_hash)
788 }
789
790 #[allow(dead_code)]
792 pub fn best_hash(&self, height: block::Height) -> Option<block::Hash> {
793 self.best_chain()?
794 .blocks
795 .get(&height)
796 .map(|prepared| prepared.hash)
797 }
798
799 #[allow(dead_code)]
801 pub fn best_tip(&self) -> Option<(block::Height, block::Hash)> {
802 let best_chain = self.best_chain()?;
803 let height = best_chain.non_finalized_tip_height();
804 let hash = best_chain.non_finalized_tip_hash();
805
806 Some((height, hash))
807 }
808
809 #[allow(dead_code)]
811 pub fn best_tip_block(&self) -> Option<&ContextuallyVerifiedBlock> {
812 let best_chain = self.best_chain()?;
813
814 best_chain.tip_block()
815 }
816
817 #[allow(dead_code)]
819 pub fn best_height_by_hash(&self, hash: block::Hash) -> Option<block::Height> {
820 let best_chain = self.best_chain()?;
821 let height = *best_chain.height_by_hash.get(&hash)?;
822 Some(height)
823 }
824
825 #[allow(dead_code)]
827 pub fn any_height_by_hash(&self, hash: block::Hash) -> Option<block::Height> {
828 for chain in self.chain_set.iter().rev() {
829 if let Some(height) = chain.height_by_hash.get(&hash) {
830 return Some(*height);
831 }
832 }
833
834 None
835 }
836
837 #[cfg(any(test, feature = "proptest-impl"))]
839 #[allow(dead_code)]
840 pub fn best_contains_sprout_nullifier(&self, sprout_nullifier: &sprout::Nullifier) -> bool {
841 self.best_chain()
842 .map(|best_chain| best_chain.sprout_nullifiers.contains_key(sprout_nullifier))
843 .unwrap_or(false)
844 }
845
846 #[cfg(any(test, feature = "proptest-impl"))]
848 #[allow(dead_code)]
849 pub fn best_contains_sapling_nullifier(
850 &self,
851 sapling_nullifier: &zebra_chain::sapling::Nullifier,
852 ) -> bool {
853 self.best_chain()
854 .map(|best_chain| {
855 best_chain
856 .sapling_nullifiers
857 .contains_key(sapling_nullifier)
858 })
859 .unwrap_or(false)
860 }
861
862 #[cfg(any(test, feature = "proptest-impl"))]
864 #[allow(dead_code)]
865 pub fn best_contains_orchard_nullifier(
866 &self,
867 orchard_nullifier: &zebra_chain::orchard::Nullifier,
868 ) -> bool {
869 self.best_chain()
870 .map(|best_chain| {
871 best_chain
872 .orchard_nullifiers
873 .contains_key(orchard_nullifier)
874 })
875 .unwrap_or(false)
876 }
877
878 pub fn best_chain(&self) -> Option<&Arc<Chain>> {
880 self.chain_iter().next()
881 }
882
883 pub fn chain_count(&self) -> usize {
885 self.chain_set.len()
886 }
887
888 pub fn is_chain_set_empty(&self) -> bool {
890 self.chain_count() == 0
891 }
892
893 pub fn invalidated_blocks(&self) -> IndexMap<Height, Arc<Vec<ContextuallyVerifiedBlock>>> {
895 self.invalidated_blocks.clone()
896 }
897
898 fn parent_chain(&self, parent_hash: block::Hash) -> Result<Arc<Chain>, ValidateContextError> {
903 match self.find_chain(|chain| chain.non_finalized_tip_hash() == parent_hash) {
904 Some(chain) => Ok(chain.clone()),
906 None => {
908 let fork_chain = self
911 .chain_set
912 .iter()
913 .rev()
914 .find_map(|chain| chain.fork(parent_hash))
915 .ok_or(ValidateContextError::NotReadyToBeCommitted)?;
916
917 Ok(Arc::new(fork_chain))
918 }
919 }
920 }
921
922 fn should_count_metrics(&self) -> bool {
924 self.should_count_metrics
925 }
926
927 fn update_metrics_for_committed_block(&self, height: block::Height, hash: block::Hash) {
929 if !self.should_count_metrics() {
930 return;
931 }
932
933 metrics::counter!("state.memory.committed.block.count").increment(1);
934 metrics::gauge!("state.memory.committed.block.height").set(height.0 as f64);
935
936 if self
937 .best_chain()
938 .expect("metrics are only updated after initialization")
939 .non_finalized_tip_hash()
940 == hash
941 {
942 metrics::counter!("state.memory.best.committed.block.count").increment(1);
943 metrics::gauge!("state.memory.best.committed.block.height").set(height.0 as f64);
944 }
945
946 self.update_metrics_for_chains();
947 }
948
949 fn update_metrics_for_chains(&self) {
951 if !self.should_count_metrics() {
952 return;
953 }
954
955 metrics::gauge!("state.memory.chain.count").set(self.chain_set.len() as f64);
956 metrics::gauge!("state.memory.best.chain.length",)
957 .set(self.best_chain_len().unwrap_or_default() as f64);
958
959 if let Some(best_chain) = self.best_chain() {
960 let value_pools = best_chain.chain_value_pools;
961 metrics::gauge!("zcash.pool.value.zatoshis", "name" => "transparent")
962 .set(u64::from(value_pools.transparent_amount()) as f64);
963 metrics::gauge!("zcash.pool.value.zatoshis", "name" => "sprout")
964 .set(u64::from(value_pools.sprout_amount()) as f64);
965 metrics::gauge!("zcash.pool.value.zatoshis", "name" => "sapling")
966 .set(u64::from(value_pools.sapling_amount()) as f64);
967 metrics::gauge!("zcash.pool.value.zatoshis", "name" => "orchard")
968 .set(u64::from(value_pools.orchard_amount()) as f64);
969 metrics::gauge!("zcash.pool.value.zatoshis", "name" => "ironwood")
970 .set(u64::from(value_pools.ironwood_amount()) as f64);
971
972 metrics::gauge!("zcash.pool.notes.created", "name" => "sprout")
973 .set(best_chain.sprout_note_commitment_tree_for_tip().count() as f64);
974 metrics::gauge!("zcash.pool.notes.created", "name" => "sapling")
975 .set(best_chain.sapling_note_commitment_tree_for_tip().count() as f64);
976 metrics::gauge!("zcash.pool.notes.created", "name" => "orchard")
977 .set(best_chain.orchard_note_commitment_tree_for_tip().count() as f64);
978 metrics::gauge!("zcash.pool.notes.created", "name" => "ironwood")
979 .set(best_chain.ironwood_note_commitment_tree_for_tip().count() as f64);
980 }
981 }
982
983 fn update_metrics_bars(&mut self) {
986 if !self.should_count_metrics() {
989 #[allow(clippy::needless_return)]
990 return;
991 }
992
993 #[cfg(feature = "progress-bar")]
994 {
995 use std::cmp::Ordering::*;
996
997 if matches!(howudoin::cancelled(), Some(true)) {
998 self.disable_metrics();
999 return;
1000 }
1001
1002 if self.chain_count_bar.is_none() {
1004 self.chain_count_bar = Some(howudoin::new_root().label("Chain Forks"));
1005 }
1006
1007 let chain_count_bar = self
1008 .chain_count_bar
1009 .as_ref()
1010 .expect("just initialized if missing");
1011 let finalized_tip_height = self
1012 .best_chain()
1013 .map(|chain| chain.non_finalized_root_height().0 - 1);
1014
1015 chain_count_bar.set_pos(u64::try_from(self.chain_count()).expect("fits in u64"));
1016 if let Some(finalized_tip_height) = finalized_tip_height {
1019 chain_count_bar.desc(format!("Finalized Root {finalized_tip_height}"));
1020 }
1021
1022 let prev_length_bars = self.chain_fork_length_bars.len();
1024
1025 match self.chain_count().cmp(&prev_length_bars) {
1026 Greater => self
1027 .chain_fork_length_bars
1028 .resize_with(self.chain_count(), || {
1029 howudoin::new_with_parent(chain_count_bar.id())
1030 }),
1031 Less => {
1032 let redundant_bars = self.chain_fork_length_bars.split_off(self.chain_count());
1033 for bar in redundant_bars {
1034 bar.close();
1035 }
1036 }
1037 Equal => {}
1038 }
1039
1040 for (chain_length_bar, chain) in
1043 std::iter::zip(self.chain_fork_length_bars.iter(), self.chain_iter())
1044 {
1045 let fork_height = chain
1046 .last_fork_height
1047 .unwrap_or_else(|| chain.non_finalized_tip_height())
1048 .0;
1049
1050 chain_length_bar
1054 .label(format!("Fork {fork_height}"))
1055 .set_pos(u64::try_from(chain.len()).expect("fits in u64"));
1056 let mut desc = String::new();
1066
1067 if let Some(recent_fork_height) = chain.recent_fork_height() {
1068 let recent_fork_length = chain
1069 .recent_fork_length()
1070 .expect("just checked recent fork height");
1071
1072 let mut plural = "s";
1073 if recent_fork_length == 1 {
1074 plural = "";
1075 }
1076
1077 desc.push_str(&format!(
1078 " at {recent_fork_height:?} + {recent_fork_length} block{plural}"
1079 ));
1080 }
1081
1082 chain_length_bar.desc(desc);
1083 }
1084 }
1085 }
1086
1087 pub fn disable_metrics(&mut self) {
1089 self.should_count_metrics = false;
1090
1091 #[cfg(feature = "progress-bar")]
1092 {
1093 let count_bar = self.chain_count_bar.take().into_iter();
1094 let fork_bars = self.chain_fork_length_bars.drain(..);
1095 count_bar.chain(fork_bars).for_each(howudoin::Tx::close);
1096 }
1097 }
1098}
1099
1100impl Drop for NonFinalizedState {
1101 fn drop(&mut self) {
1102 self.disable_metrics();
1103 }
1104}