Skip to main content

zebra_state/service/
non_finalized_state.rs

1//! Non-finalized chain state management as defined by [RFC0005]
2//!
3//! [RFC0005]: https://zebra.zfnd.org/dev/rfcs/0005-state-updates.html
4
5use 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
44/// The state of the chains in memory, including queued blocks.
45///
46/// Clones of the non-finalized state contain independent copies of the chains.
47/// This is different from `FinalizedState::clone()`,
48/// which returns a shared reference to the database.
49///
50/// Most chain data is clone-on-write using [`Arc`].
51pub struct NonFinalizedState {
52    // Chain Data
53    //
54    /// Verified, non-finalized chains, in ascending work order.
55    ///
56    /// The best chain is [`NonFinalizedState::best_chain()`], or `chain_iter().next()`.
57    /// Using `chain_set.last()` or `chain_set.iter().next_back()` is deprecated,
58    /// callers should migrate to `chain_iter().next()`.
59    chain_set: BTreeSet<Arc<Chain>>,
60
61    /// Blocks that have been invalidated in, and removed from, the non finalized
62    /// state.
63    invalidated_blocks: IndexMap<Height, Arc<Vec<ContextuallyVerifiedBlock>>>,
64
65    // Configuration
66    //
67    /// The configured Zcash network.
68    pub network: Network,
69
70    // Diagnostics
71    //
72    /// Configures the non-finalized state to count metrics.
73    ///
74    /// Used for skipping metrics and progress bars when testing block proposals
75    /// with a commit to a cloned non-finalized state.
76    //
77    // TODO: make this field private and set it via an argument to NonFinalizedState::new()
78    should_count_metrics: bool,
79
80    /// Number of chain forks transmitter.
81    #[cfg(feature = "progress-bar")]
82    chain_count_bar: Option<howudoin::Tx>,
83
84    /// A chain fork length transmitter for each [`Chain`] in [`chain_set`](Self.chain_set).
85    ///
86    /// Because `chain_set` contains `Arc<Chain>`s, it is difficult to update the metrics state
87    /// on each chain. ([`Arc`]s are read-only, and we don't want to clone them just for metrics.)
88    #[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            // Don't track progress in clones.
113            #[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    /// Returns a new non-finalized state for `network`.
123    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    /// Writes the current non-finalized state to the backup directory at `backup_dir_path`.
137    ///
138    /// Reads the existing backup directory contents, writes any blocks that are in the
139    /// non-finalized state but missing from the backup, and deletes any backup files that
140    /// are no longer present in the non-finalized state.
141    ///
142    /// This method performs blocking I/O and should only be called from a blocking context.
143    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    /// Accepts an optional path to the non-finalized state backup directory and a handle to the database.
150    ///
151    /// If a backup directory path is provided:
152    /// - Creates a new backup directory at the provided path if none exists,
153    /// - Restores non-finalized blocks from the backup directory, if any, and
154    /// - Unless `skip_backup_task` is true, spawns a task that updates the non-finalized
155    ///   backup cache with the latest non-finalized state sent to the returned watch channel.
156    ///
157    /// Returns the non-finalized state with a watch channel sender and receiver.
158    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                // Create a new backup directory if none exists
202                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    /// Is the internal state of `self` the same as `other`?
239    ///
240    /// [`Chain`] has a custom [`Eq`] implementation based on proof of work,
241    /// which is used to select the best chain. So we can't derive [`Eq`] for [`NonFinalizedState`].
242    ///
243    /// Unlike the custom trait impl, this method returns `true` if the entire internal state
244    /// of two non-finalized states is equal.
245    ///
246    /// If the internal states are different, it returns `false`,
247    /// even if the chains and blocks are equal.
248    #[cfg(any(test, feature = "proptest-impl"))]
249    #[allow(dead_code)]
250    pub fn eq_internal_state(&self, other: &NonFinalizedState) -> bool {
251        // this method must be updated every time a consensus-critical field is added to NonFinalizedState
252        // (diagnostic fields can be ignored)
253
254        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    /// Returns an iterator over the non-finalized chains, with the best chain first.
264    //
265    // TODO: replace chain_set.iter().rev() with this method
266    pub fn chain_iter(&self) -> impl Iterator<Item = &Arc<Chain>> {
267        self.chain_set.iter().rev()
268    }
269
270    /// Insert `chain` into `self.chain_set`, apply `chain_filter` to the chains,
271    /// then limit the number of tracked chains.
272    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            // The first chain is the chain with the lowest work.
282            self.chain_set.pop_first();
283        }
284
285        self.update_metrics_bars();
286    }
287
288    /// Insert `chain` into `self.chain_set`, then limit the number of tracked chains.
289    fn insert(&mut self, chain: Arc<Chain>) {
290        self.insert_with(chain, |_ignored_chain| { /* no filter */ })
291    }
292
293    /// Finalize the lowest height block in the non-finalized portion of the best
294    /// chain and update all side-chains to match.
295    pub fn finalize(&mut self) -> FinalizableBlock {
296        // Chain::cmp uses the partial cumulative work, the tip's receipt time, and the
297        // hash of the tip block. None of these fields has interior mutability.
298        // (And when the tip block is dropped for a chain, the chain is also dropped.)
299        #[allow(clippy::mutable_key_type)]
300        let chains = mem::take(&mut self.chain_set);
301        let mut chains = chains.into_iter();
302
303        // extract best chain
304        let mut best_chain = chains.next_back().expect("there's at least one chain");
305
306        // clone if required
307        let mut_best_chain = Arc::make_mut(&mut best_chain);
308
309        // extract the rest into side_chains so they can be mutated
310        let side_chains = chains;
311
312        // Pop the lowest height block from the best chain to be finalized, and
313        // also obtain its associated treestate.
314        let (best_chain_root, root_treestate) = mut_best_chain.pop_root();
315
316        // add best_chain back to `self.chain_set`
317        if !best_chain.is_empty() {
318            self.insert(best_chain);
319        }
320
321        // for each remaining chain in side_chains
322        for mut side_chain in side_chains.rev() {
323            if side_chain.non_finalized_root_hash() != best_chain_root.hash {
324                // If we popped the root, the chain would be empty or orphaned,
325                // so just drop it now.
326                drop(side_chain);
327
328                continue;
329            }
330
331            // otherwise, the popped root block is the same as the finalizing block
332
333            // clone if required
334            let mut_side_chain = Arc::make_mut(&mut side_chain);
335
336            // remove the first block from `chain`
337            let (side_chain_root, _treestate) = mut_side_chain.pop_root();
338            assert_eq!(side_chain_root.hash, best_chain_root.hash);
339
340            // add the chain back to `self.chain_set`
341            if !side_chain.is_empty() {
342                self.insert(side_chain);
343            }
344        }
345
346        // Remove all invalidated_blocks at or below the finalized height
347        self.invalidated_blocks
348            .retain(|height, _blocks| *height >= best_chain_root.height);
349
350        self.update_metrics_for_chains();
351
352        // Add the treestate to the finalized block.
353        FinalizableBlock::new(best_chain_root, root_treestate)
354    }
355
356    /// Commit block to the non-finalized state, on top of:
357    /// - an existing chain's tip, or
358    /// - a newly forked chain.
359    #[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        // If the block is invalid, return the error,
371        // and drop the cloned parent Arc, or newly created chain fork.
372        let modified_chain = self.validate_and_commit(parent_chain, prepared, finalized_state)?;
373
374        // If the block is valid:
375        // - add the new chain fork or updated chain to the set of recent chains
376        // - remove the parent chain, if it was in the chain set
377        //   (if it was a newly created fork, it won't be in the chain set)
378        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    /// Invalidate block with hash `block_hash` and all descendants from the non-finalized state. Insert
388    /// the new chain into the chain_set and discard the previous.
389    ///
390    /// Returns an error without modifying the state if the block is the root of the only chain.
391    #[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            // Add the new chain fork or updated chain to the set of recent chains, and
410            // remove the chain containing the hash of the block from chain set
411            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        // TODO: Allow for invalidating multiple block hashes at a given height (#9552).
419        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    /// Reconsiders a previously invalidated block and its descendants into the non-finalized state
439    /// based on a block_hash. Reconsidered blocks are inserted into the previous chain and re-inserted
440    /// into the chain_set.
441    #[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        // Locate the record but keep it live until replay succeeds, so a
448        // recoverable error can't lose it; it is `shift_remove`d atomically with
449        // the insert below.
450        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        // Find and fork the parent chain of the invalidated_root. Update the parent chain
468        // with the invalidated_descendants
469        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        // If the parent is the tip of the finalized_state we create a new chain and insert it
476        // into the non finalized state
477        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            // The parent is not the finalized_tip and still exist in the NonFinalizedState
490            // or else we return an error due to the parent not existing in the NonFinalizedState
491            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        // All fallible steps have succeeded; remove the invalidation record
505        // atomically with installing the restored chain so a failed attempt
506        // does not destroy the record.
507        self.invalidated_blocks.shift_remove(&height);
508
509        // Only track invalidated_blocks that are not yet finalized. Once blocks are finalized (below the best_chain_root_height)
510        // we can discard the block.
511        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    /// Commit block to the non-finalized state as a new chain where its parent
526    /// is the finalized tip.
527    #[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        // TODO: fix tests that don't initialize the finalized state
537        #[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        // If the block is invalid, return the error, and drop the newly created chain fork
553        let chain = self.validate_and_commit(Arc::new(chain), prepared, finalized_state)?;
554
555        // If the block is valid, add the new chain fork to the set of recent chains.
556        self.insert(chain);
557        self.update_metrics_for_committed_block(height, hash);
558
559        Ok(())
560    }
561
562    /// Contextually validate `prepared` using `finalized_state`.
563    /// If validation succeeds, push `prepared` onto `new_chain`.
564    ///
565    /// `new_chain` should start as a clone of the parent chain fork,
566    /// or the finalized tip.
567    #[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        // Reads from disk
585        //
586        // TODO: if these disk reads show up in profiles, run them in parallel, using std::thread::spawn()
587        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        // Reads from disk
595        check::anchors::block_sapling_orchard_anchors_refer_to_final_treestates(
596            finalized_state,
597            &new_chain,
598            &prepared,
599        )?;
600
601        // Reads from disk
602        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        // The semantic verifier defers parent-dependent payouts to this exact-parent check.
640        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    /// Validate `contextual` and update `new_chain`, doing CPU-intensive work in parallel batches.
653    #[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        // Clone function arguments for different threads
665        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            // We're pretty sure the new block is valid,
693            // so clone the inner chain if needed, then add the new block.
694            //
695            // Pushing a block onto a Chain can launch additional parallel batches.
696            // TODO: should we pass _scope into Chain::push()?
697            scope.spawn_fifo(|_scope| {
698                // TODO: Replace with Arc::unwrap_or_clone() when it stabilises:
699                // https://github.com/rust-lang/rust/issues/93610
700                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        // Don't return the updated Chain unless all the parallel results were Ok
707        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    /// Returns the length of the non-finalized portion of the current best chain
714    /// or `None` if the best chain has no blocks.
715    pub fn best_chain_len(&self) -> Option<u32> {
716        // This `as` can't overflow because the number of blocks in the chain is limited to i32::MAX,
717        // and the non-finalized chain is further limited by the rollback window
718        // (`MAX_BLOCK_REORG_HEIGHT`, currently 1000 blocks).
719        Some(self.best_chain()?.blocks.len() as u32)
720    }
721
722    /// Returns the root height of the non-finalized state, if the non-finalized state is not empty.
723    pub fn root_height(&self) -> Option<block::Height> {
724        self.best_chain()
725            .map(|chain| chain.non_finalized_root_height())
726    }
727
728    /// Returns `true` if `hash` is contained in the non-finalized portion of any
729    /// known chain.
730    #[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    /// Returns the first chain satisfying the given predicate.
739    ///
740    /// If multiple chains satisfy the predicate, returns the chain with the highest difficulty.
741    /// (Using the first-received, then tip block hash tie-breakers.)
742    pub fn find_chain<P>(&self, mut predicate: P) -> Option<Arc<Chain>>
743    where
744        P: FnMut(&Chain) -> bool,
745    {
746        // Reverse the iteration order, to find highest difficulty chains first.
747        self.chain_set
748            .iter()
749            .rev()
750            .find(|chain| predicate(chain))
751            .cloned()
752    }
753
754    /// Returns the [`transparent::Utxo`] pointed to by the given
755    /// [`transparent::OutPoint`] if it is present in any chain.
756    ///
757    /// UTXOs are returned regardless of whether they have been spent.
758    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    /// Returns the `block` with the given hash in any chain.
766    #[allow(dead_code)]
767    pub fn any_block_by_hash(&self, hash: block::Hash) -> Option<Arc<Block>> {
768        // This performs efficiently because the number of chains is limited to 10.
769        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    /// Returns the previous block hash for the given block hash in any chain.
783    #[allow(dead_code)]
784    pub fn any_prev_block_hash_for_hash(&self, hash: block::Hash) -> Option<block::Hash> {
785        // This performs efficiently because the blocks are in memory.
786        self.any_block_by_hash(hash)
787            .map(|block| block.header.previous_block_hash)
788    }
789
790    /// Returns the hash for a given `block::Height` if it is present in the best chain.
791    #[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    /// Returns the tip of the best chain.
800    #[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    /// Returns the block at the tip of the best chain.
810    #[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    /// Returns the height of `hash` in the best chain.
818    #[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    /// Returns the height of `hash` in any chain.
826    #[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    /// Returns `true` if the best chain contains `sprout_nullifier`.
838    #[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    /// Returns `true` if the best chain contains `sapling_nullifier`.
847    #[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    /// Returns `true` if the best chain contains `orchard_nullifier`.
863    #[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    /// Return the non-finalized portion of the current best chain.
879    pub fn best_chain(&self) -> Option<&Arc<Chain>> {
880        self.chain_iter().next()
881    }
882
883    /// Return the number of chains.
884    pub fn chain_count(&self) -> usize {
885        self.chain_set.len()
886    }
887
888    /// Returns true if this [`NonFinalizedState`] contains no chains.
889    pub fn is_chain_set_empty(&self) -> bool {
890        self.chain_count() == 0
891    }
892
893    /// Return the invalidated blocks.
894    pub fn invalidated_blocks(&self) -> IndexMap<Height, Arc<Vec<ContextuallyVerifiedBlock>>> {
895        self.invalidated_blocks.clone()
896    }
897
898    /// Return the chain whose tip block hash is `parent_hash`.
899    ///
900    /// The chain can be an existing chain in the non-finalized state, or a freshly
901    /// created fork.
902    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            // Clone the existing Arc<Chain> in the non-finalized state
905            Some(chain) => Ok(chain.clone()),
906            // Create a new fork
907            None => {
908                // Check the lowest difficulty chains first,
909                // because the fork could be closer to their tip.
910                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    /// Should this `NonFinalizedState` instance track metrics and progress bars?
923    fn should_count_metrics(&self) -> bool {
924        self.should_count_metrics
925    }
926
927    /// Update the metrics after `block` is committed
928    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    /// Update the metrics after `self.chain_set` is modified
950    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    /// Update the progress bars after any chain is modified.
984    /// This includes both chain forks and committed blocks.
985    fn update_metrics_bars(&mut self) {
986        // TODO: make chain_count_bar interior mutable, move to update_metrics_for_committed_block()
987
988        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            // Update the chain count bar
1003            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            // .set_len(u64::try_from(MAX_NON_FINALIZED_CHAIN_FORKS).expect("fits in u64"));
1017
1018            if let Some(finalized_tip_height) = finalized_tip_height {
1019                chain_count_bar.desc(format!("Finalized Root {finalized_tip_height}"));
1020            }
1021
1022            // Update each chain length bar, creating or deleting bars as needed
1023            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            // It doesn't matter what chain the bar was previously used for,
1041            // because we update everything based on the latest chain in that position.
1042            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                // We need to initialize and set all the values of the bar here, because:
1051                // - the bar might have been newly created, or
1052                // - the chain this bar was previously assigned to might have changed position.
1053                chain_length_bar
1054                    .label(format!("Fork {fork_height}"))
1055                    .set_pos(u64::try_from(chain.len()).expect("fits in u64"));
1056                // TODO: should this be MAX_BLOCK_REORG_HEIGHT?
1057                // .set_len(u64::from(
1058                //     zebra_chain::transparent::MIN_TRANSPARENT_COINBASE_MATURITY,
1059                // ));
1060
1061                // TODO: store work in the finalized state for each height (#7109),
1062                //       and show the full chain work here, like `zcashd` (#7110)
1063                //
1064                // For now, we don't show any work here, see the deleted code in PR #7087.
1065                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    /// Stop tracking metrics for this non-finalized state and all its chains.
1088    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}