Skip to main content

zebra_state/service/
chain_tip.rs

1//! Access to Zebra chain tip information.
2//!
3//! Zebra has 3 different interfaces for access to chain tip information:
4//! * [zebra_state::Request](crate::request): [tower::Service] requests about chain state,
5//! * [LatestChainTip] for efficient access to the current best tip, and
6//! * [ChainTipChange] to `await` specific changes to the chain tip.
7
8use std::{fmt, sync::Arc};
9
10use chrono::{DateTime, Utc};
11use futures::TryFutureExt;
12use tokio::sync::watch;
13use tracing::{field, instrument};
14
15use zebra_chain::{
16    block,
17    chain_tip::ChainTip,
18    parameters::{Network, NetworkUpgrade},
19    transaction::{self, Transaction},
20};
21
22use crate::{
23    request::ContextuallyVerifiedBlock, service::watch_receiver::WatchReceiver, BoxError,
24    CheckpointVerifiedBlock, SemanticallyVerifiedBlock,
25};
26
27use TipAction::*;
28
29#[cfg(any(test, feature = "proptest-impl"))]
30use proptest_derive::Arbitrary;
31
32#[cfg(any(test, feature = "proptest-impl"))]
33use zebra_chain::serialization::arbitrary::datetime_full;
34
35#[cfg(test)]
36mod tests;
37
38/// The internal watch channel data type for [`ChainTipSender`], [`LatestChainTip`],
39/// and [`ChainTipChange`].
40type ChainTipData = Option<ChainTipBlock>;
41
42/// A chain tip block, with precalculated block data.
43///
44/// Used to efficiently update [`ChainTipSender`], [`LatestChainTip`],
45/// and [`ChainTipChange`].
46#[derive(Clone, Debug, PartialEq, Eq)]
47#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
48pub struct ChainTipBlock {
49    /// The hash of the best chain tip block.
50    pub hash: block::Hash,
51
52    /// The height of the best chain tip block.
53    pub height: block::Height,
54
55    /// The network block time of the best chain tip block.
56    #[cfg_attr(
57        any(test, feature = "proptest-impl"),
58        proptest(strategy = "datetime_full()")
59    )]
60    pub time: DateTime<Utc>,
61
62    /// The block transactions.
63    pub transactions: Vec<Arc<Transaction>>,
64
65    /// The mined transaction IDs of the transactions in `block`,
66    /// in the same order as `block.transactions`.
67    pub transaction_hashes: Arc<[transaction::Hash]>,
68
69    /// The hash of the previous block in the best chain.
70    /// This block is immediately behind the best chain tip.
71    ///
72    /// ## Note
73    ///
74    /// If the best chain fork has changed, or some blocks have been skipped,
75    /// this hash will be different to the last returned `ChainTipBlock.hash`.
76    pub previous_block_hash: block::Hash,
77}
78
79impl fmt::Display for ChainTipBlock {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        f.debug_struct("ChainTipBlock")
82            .field("height", &self.height)
83            .field("hash", &self.hash)
84            .field("transactions", &self.transactions.len())
85            .finish()
86    }
87}
88
89impl From<ContextuallyVerifiedBlock> for ChainTipBlock {
90    fn from(contextually_valid: ContextuallyVerifiedBlock) -> Self {
91        let ContextuallyVerifiedBlock {
92            block,
93            hash,
94            height,
95            transaction_hashes,
96            ..
97        } = contextually_valid;
98
99        Self {
100            hash,
101            height,
102            time: block.header.time,
103            transactions: block.transactions.clone(),
104            transaction_hashes,
105            previous_block_hash: block.header.previous_block_hash,
106        }
107    }
108}
109
110impl From<SemanticallyVerifiedBlock> for ChainTipBlock {
111    fn from(prepared: SemanticallyVerifiedBlock) -> Self {
112        let SemanticallyVerifiedBlock {
113            block,
114            hash,
115            height,
116            new_outputs: _,
117            transaction_hashes,
118            received_time: _,
119        } = prepared;
120
121        Self {
122            hash,
123            height,
124            time: block.header.time,
125            transactions: block.transactions.clone(),
126            transaction_hashes,
127            previous_block_hash: block.header.previous_block_hash,
128        }
129    }
130}
131
132impl From<CheckpointVerifiedBlock> for ChainTipBlock {
133    fn from(CheckpointVerifiedBlock(prepared): CheckpointVerifiedBlock) -> Self {
134        prepared.into()
135    }
136}
137
138/// A sender for changes to the non-finalized and finalized chain tips.
139#[derive(Debug)]
140pub struct ChainTipSender {
141    /// Whether the selected best tip is from the non-finalized state.
142    ///
143    /// Cleared when the write task falls back to the finalized tip.
144    has_non_finalized_tip: bool,
145
146    /// The sender channel for chain tip data.
147    sender: watch::Sender<ChainTipData>,
148}
149
150impl ChainTipSender {
151    /// Create new linked instances of [`ChainTipSender`], [`LatestChainTip`], and [`ChainTipChange`],
152    /// using an `initial_tip` and a [`Network`].
153    #[instrument(skip(initial_tip), fields(new_height, new_hash))]
154    pub fn new(
155        initial_tip: impl Into<Option<ChainTipBlock>>,
156        network: &Network,
157    ) -> (Self, LatestChainTip, ChainTipChange) {
158        let initial_tip = initial_tip.into();
159        Self::record_new_tip(&initial_tip);
160
161        let (sender, receiver) = watch::channel(None);
162
163        let mut sender = ChainTipSender {
164            has_non_finalized_tip: false,
165            sender,
166        };
167
168        let current = LatestChainTip::new(receiver);
169        let change = ChainTipChange::new(current.clone(), network);
170
171        sender.update(initial_tip);
172
173        (sender, current, change)
174    }
175
176    /// Returns a clone of itself for sending finalized tip changes,
177    /// used by `TrustedChainSync` in `zebra-rpc`.
178    pub fn finalized_sender(&self) -> Self {
179        Self {
180            has_non_finalized_tip: false,
181            sender: self.sender.clone(),
182        }
183    }
184
185    /// Update the latest finalized tip.
186    ///
187    /// May trigger an update to the best tip.
188    #[instrument(
189        skip(self, new_tip),
190        fields(old_has_non_finalized_tip, old_height, old_hash, new_height, new_hash)
191    )]
192    pub fn set_finalized_tip(&mut self, new_tip: impl Into<Option<ChainTipBlock>> + Clone) {
193        let new_tip = new_tip.into();
194        self.record_fields(&new_tip);
195
196        if !self.has_non_finalized_tip {
197            self.update(new_tip);
198        }
199    }
200
201    /// Update the latest non-finalized tip.
202    ///
203    /// May trigger an update to the best tip.
204    #[instrument(
205        skip(self, new_tip),
206        fields(old_has_non_finalized_tip, old_height, old_hash, new_height, new_hash)
207    )]
208    pub fn set_best_non_finalized_tip(
209        &mut self,
210        new_tip: impl Into<Option<ChainTipBlock>> + Clone,
211    ) {
212        let new_tip = new_tip.into();
213        self.record_fields(&new_tip);
214
215        // An absent update does not change the selected tip. The write task uses
216        // `clear_best_non_finalized_tip` to explicitly fall back to the finalized state.
217        if new_tip.is_some() {
218            self.has_non_finalized_tip = true;
219            self.update(new_tip)
220        }
221    }
222
223    /// Select the current finalized tip when the non-finalized state is empty.
224    ///
225    /// The caller supplies the current database tip, which may have advanced since
226    /// the last checkpointed block. If both states are empty, clear any old tip.
227    pub(crate) fn clear_best_non_finalized_tip(&mut self, finalized_tip: Option<ChainTipBlock>) {
228        self.has_non_finalized_tip = false;
229        if finalized_tip.is_some() {
230            self.update(finalized_tip);
231        } else {
232            let _ = self.sender.send(None);
233        }
234    }
235
236    /// Possibly send an update to listeners.
237    ///
238    /// An update is only sent if the current best tip is different from the last best tip
239    /// that was sent.
240    fn update(&mut self, new_tip: Option<ChainTipBlock>) {
241        // Correctness: the `self.sender.borrow()` must not be placed in a `let` binding to prevent
242        // a read-lock being created and living beyond the `self.sender.send(..)` call. If that
243        // happens, the `send` method will attempt to obtain a write-lock and will dead-lock.
244        // Without the binding, the guard is dropped at the end of the expression.
245        let active_hash = self
246            .sender
247            .borrow()
248            .as_ref()
249            .map(|active_value| active_value.hash);
250
251        let needs_update = match (new_tip.as_ref(), active_hash) {
252            // since the blocks have been contextually validated,
253            // we know their hashes cover all the block data
254            (Some(new_tip), Some(active_hash)) => new_tip.hash != active_hash,
255            (Some(_new_tip), None) => true,
256            (None, _active_value_hash) => false,
257        };
258
259        if needs_update {
260            let _ = self.sender.send(new_tip);
261        }
262    }
263
264    /// Record `new_tip` in the current span.
265    ///
266    /// Callers should create a new span with empty `new_height` and `new_hash` fields.
267    fn record_new_tip(new_tip: &Option<ChainTipBlock>) {
268        Self::record_tip(&tracing::Span::current(), "new", new_tip);
269    }
270
271    /// Record `new_tip` and the fields from `self` in the current span.
272    ///
273    /// The fields recorded are:
274    ///
275    /// - `new_height`
276    /// - `new_hash`
277    /// - `old_height`
278    /// - `old_hash`
279    /// - `old_has_non_finalized_tip`
280    ///
281    /// Callers should create a new span with the empty fields described above.
282    fn record_fields(&self, new_tip: &Option<ChainTipBlock>) {
283        let span = tracing::Span::current();
284
285        let old_tip = &*self.sender.borrow();
286
287        Self::record_tip(&span, "new", new_tip);
288        Self::record_tip(&span, "old", old_tip);
289
290        span.record(
291            "old_has_non_finalized_tip",
292            field::debug(self.has_non_finalized_tip),
293        );
294    }
295
296    /// Record `tip` into `span` using the `prefix` to name the fields.
297    ///
298    /// Callers should create a new span with empty `{prefix}_height` and `{prefix}_hash` fields.
299    fn record_tip(span: &tracing::Span, prefix: &str, tip: &Option<ChainTipBlock>) {
300        let height = tip.as_ref().map(|block| block.height);
301        let hash = tip.as_ref().map(|block| block.hash);
302
303        span.record(format!("{prefix}_height").as_str(), field::debug(height));
304        span.record(format!("{prefix}_hash").as_str(), field::debug(hash));
305    }
306}
307
308/// Efficient access to the state's current best chain tip.
309///
310/// Each method returns data from the latest tip,
311/// regardless of how many times you call it.
312///
313/// Cloned instances provide identical tip data.
314///
315/// The chain tip data is based on:
316/// * the best non-finalized chain tip, if available, or
317/// * the finalized tip.
318///
319/// ## Note
320///
321/// If a lot of blocks are committed at the same time,
322/// the latest tip will skip some blocks in the chain.
323#[derive(Clone, Debug)]
324pub struct LatestChainTip {
325    /// The receiver for the current chain tip's data.
326    receiver: WatchReceiver<ChainTipData>,
327}
328
329impl LatestChainTip {
330    /// Create a new [`LatestChainTip`] from a watch channel receiver.
331    fn new(receiver: watch::Receiver<ChainTipData>) -> Self {
332        Self {
333            receiver: WatchReceiver::new(receiver),
334        }
335    }
336
337    /// Maps the current data `ChainTipData` to `Option<U>`
338    /// by applying a function to the watched value,
339    /// while holding the receiver lock as briefly as possible.
340    ///
341    /// This helper method is a shorter way to borrow the value from the [`watch::Receiver`] and
342    /// extract some information from it, while also adding the current chain tip block's fields as
343    /// records to the current span.
344    ///
345    /// A single read lock is acquired to clone `T`, and then released after the clone.
346    /// See the performance note on [`WatchReceiver::with_watch_data`].
347    ///
348    /// Does not mark the watched data as seen.
349    ///
350    /// # Correctness
351    ///
352    /// To avoid deadlocks, see the correctness note on [`WatchReceiver::with_watch_data`].
353    fn with_chain_tip_block<U, F>(&self, f: F) -> Option<U>
354    where
355        F: FnOnce(&ChainTipBlock) -> U,
356    {
357        let span = tracing::Span::current();
358
359        let register_span_fields = |chain_tip_block: Option<&ChainTipBlock>| {
360            span.record(
361                "height",
362                tracing::field::debug(chain_tip_block.map(|block| block.height)),
363            );
364            span.record(
365                "hash",
366                tracing::field::debug(chain_tip_block.map(|block| block.hash)),
367            );
368            span.record(
369                "time",
370                tracing::field::debug(chain_tip_block.map(|block| block.time)),
371            );
372            span.record(
373                "previous_hash",
374                tracing::field::debug(chain_tip_block.map(|block| block.previous_block_hash)),
375            );
376            span.record(
377                "transaction_count",
378                tracing::field::debug(chain_tip_block.map(|block| block.transaction_hashes.len())),
379            );
380        };
381
382        self.receiver.with_watch_data(|chain_tip_block| {
383            // TODO: replace with Option::inspect when it stabilises
384            //       https://github.com/rust-lang/rust/issues/91345
385            register_span_fields(chain_tip_block.as_ref());
386
387            chain_tip_block.as_ref().map(f)
388        })
389    }
390}
391
392impl ChainTip for LatestChainTip {
393    #[instrument(skip(self))]
394    fn best_tip_height(&self) -> Option<block::Height> {
395        self.with_chain_tip_block(|block| block.height)
396    }
397
398    #[instrument(skip(self))]
399    fn best_tip_hash(&self) -> Option<block::Hash> {
400        self.with_chain_tip_block(|block| block.hash)
401    }
402
403    #[instrument(skip(self))]
404    fn best_tip_height_and_hash(&self) -> Option<(block::Height, block::Hash)> {
405        self.with_chain_tip_block(|block| (block.height, block.hash))
406    }
407
408    #[instrument(skip(self))]
409    fn best_tip_block_time(&self) -> Option<DateTime<Utc>> {
410        self.with_chain_tip_block(|block| block.time)
411    }
412
413    #[instrument(skip(self))]
414    fn best_tip_height_and_block_time(&self) -> Option<(block::Height, DateTime<Utc>)> {
415        self.with_chain_tip_block(|block| (block.height, block.time))
416    }
417
418    #[instrument(skip(self))]
419    fn best_tip_mined_transaction_ids(&self) -> Arc<[transaction::Hash]> {
420        self.with_chain_tip_block(|block| block.transaction_hashes.clone())
421            .unwrap_or_else(|| Arc::new([]))
422    }
423
424    /// Returns when the state tip changes.
425    ///
426    /// Marks the state tip as seen when the returned future completes.
427    #[instrument(skip(self))]
428    async fn best_tip_changed(&mut self) -> Result<(), BoxError> {
429        self.receiver.changed().err_into().await
430    }
431
432    /// Mark the current best state tip as seen.
433    fn mark_best_tip_seen(&mut self) {
434        self.receiver.mark_as_seen();
435    }
436}
437
438/// A chain tip change monitor.
439///
440/// Awaits changes and resets of the state's best chain tip,
441/// returning the latest [`TipAction`] once the state is updated.
442///
443/// Each cloned instance separately tracks the last block data it provided. If
444/// the best chain fork has changed since the last tip change on that instance,
445/// it returns a [`Reset`].
446///
447/// The chain tip data is based on:
448/// * the best non-finalized chain tip, if available, or
449/// * the finalized tip.
450#[derive(Debug)]
451pub struct ChainTipChange {
452    /// The receiver for the current chain tip's data.
453    latest_chain_tip: LatestChainTip,
454
455    /// The most recent [`block::Hash`] provided by this instance.
456    ///
457    /// ## Note
458    ///
459    /// If the best chain fork has changed, or some blocks have been skipped,
460    /// this hash will be different to the last returned `ChainTipBlock.hash`.
461    last_change_hash: Option<block::Hash>,
462
463    /// The network for the chain tip.
464    network: Network,
465}
466
467/// Actions that we can take in response to a [`ChainTipChange`].
468#[derive(Clone, Debug, PartialEq, Eq)]
469pub enum TipAction {
470    /// The chain tip was updated continuously,
471    /// using a child `block` of the previous block.
472    ///
473    /// The genesis block action is a `Grow`.
474    Grow {
475        /// Information about the block used to grow the chain.
476        block: ChainTipBlock,
477    },
478
479    /// The chain tip was reset to a block with `height` and `hash`.
480    ///
481    /// Resets can happen for different reasons:
482    /// - a newly created or cloned [`ChainTipChange`], which is behind the
483    ///   current tip,
484    /// - extending the chain with a network upgrade activation block,
485    /// - switching to a different best [`Chain`][1], also known as a rollback, and
486    /// - receiving multiple blocks since the previous change.
487    ///
488    /// To keep the code and tests simple, Zebra performs the same reset
489    /// actions, regardless of the reset reason.
490    ///
491    /// `Reset`s do not have the transaction hashes from the tip block, because
492    /// all transactions should be cleared by a reset.
493    ///
494    /// [1]: super::non_finalized_state::Chain
495    Reset {
496        /// The block height of the tip, after the chain reset.
497        height: block::Height,
498
499        /// The block hash of the tip, after the chain reset.
500        ///
501        /// Mainly useful for logging and debugging.
502        hash: block::Hash,
503    },
504}
505
506impl ChainTipChange {
507    /// Wait until the tip has changed, then return the corresponding [`TipAction`].
508    ///
509    /// The returned action describes how the tip has changed
510    /// since the last call to this method.
511    ///
512    /// If there have been no changes since the last time this method was called,
513    /// it waits for the next tip change before returning.
514    ///
515    /// If there have been multiple changes since the last time this method was called,
516    /// they are combined into a single [`TipAction::Reset`].
517    ///
518    /// Returns an error if communication with the state is lost.
519    ///
520    /// ## Note
521    ///
522    /// If a lot of blocks are committed at the same time,
523    /// the change will skip some blocks, and return a [`Reset`].
524    #[instrument(
525        skip(self),
526        fields(
527            last_change_hash = ?self.last_change_hash,
528            network = ?self.network,
529        ))]
530    pub async fn wait_for_tip_change(&mut self) -> Result<TipAction, watch::error::RecvError> {
531        let block = self.tip_block_change().await?;
532
533        let action = self.action(block.clone());
534
535        self.last_change_hash = Some(block.hash);
536
537        Ok(action)
538    }
539
540    /// Returns:
541    /// - `Some(`[`TipAction`]`)` if there has been a change since the last time the method was called.
542    /// - `None` if there has been no change.
543    ///
544    /// See [`Self::wait_for_tip_change`] for details.
545    #[instrument(
546        skip(self),
547        fields(
548            last_change_hash = ?self.last_change_hash,
549            network = ?self.network,
550        ))]
551    pub fn last_tip_change(&mut self) -> Option<TipAction> {
552        let block = self.latest_chain_tip.with_chain_tip_block(|block| {
553            if Some(block.hash) != self.last_change_hash {
554                Some(block.clone())
555            } else {
556                // Ignore an unchanged tip.
557                None
558            }
559        })??;
560
561        let block_hash = block.hash;
562        let tip_action = self.action(block);
563
564        self.last_change_hash = Some(block_hash);
565
566        Some(tip_action)
567    }
568
569    /// Sets the `last_change_hash` as the provided hash.
570    pub fn mark_last_change_hash(&mut self, hash: block::Hash) {
571        self.last_change_hash = Some(hash);
572    }
573
574    /// Return an action based on `block` and the last change we returned.
575    fn action(&self, block: ChainTipBlock) -> TipAction {
576        // check for an edge case that's dealt with by other code
577        assert!(
578            Some(block.hash) != self.last_change_hash,
579            "ChainTipSender and ChainTipChange ignore unchanged tips"
580        );
581
582        // If the previous block hash doesn't match, reset.
583        // We've either:
584        // - just initialized this instance,
585        // - changed the best chain to another fork (a rollback), or
586        // - skipped some blocks in the best chain.
587        //
588        // Consensus rules:
589        //
590        // > It is possible for a reorganization to occur
591        // > that rolls back from after the activation height, to before that height.
592        // > This can handled in the same way as any regular chain orphaning or reorganization,
593        // > as long as the new chain is valid.
594        //
595        // https://zips.z.cash/zip-0200#chain-reorganization
596
597        // If the block *before* a network upgrade activation block becomes the tip, reset.
598        //
599        // Consensus rules:
600        //
601        // > When the current chain tip height reaches ACTIVATION_HEIGHT,
602        // > the node's local transaction memory pool SHOULD be cleared of transactions
603        // > that will never be valid on the post-upgrade consensus branch.
604        //
605        // https://zips.z.cash/zip-0200#memory-pool
606        //
607        // ZIP-200 phrases this as "when the tip reaches ACTIVATION_HEIGHT", but the mempool
608        // verifies its transactions against the *next* block, i.e. the child of the current tip.
609        // A transaction in the mempool becomes invalid as soon as the next block it could be
610        // mined into is the activation block, which happens when the tip reaches
611        // `ACTIVATION_HEIGHT - 1`. So we reset when the *next* height is an activation height,
612        // one block earlier than the literal wording, so the mempool is already cleared by the
613        // time the activation block is mined on top of the tip.
614        //
615        // Skipped blocks can include network upgrade activation blocks.
616        // Fork changes can activate or deactivate a network upgrade.
617        // So we must perform the same actions for network upgrades and skipped blocks.
618        //
619        // The soft fork that temporarily disables Orchard actions is not a network upgrade, but
620        // it has the same memory-pool requirement and is verified against the next height in the
621        // same way, so we also reset when the next height is its activation height.
622        //
623        // The next height always exists because the chain tip height is far below `Height::MAX`.
624        let next_height = block
625            .height
626            .next()
627            .expect("chain tip height is far below Height::MAX");
628        if Some(block.previous_block_hash) != self.last_change_hash
629            || NetworkUpgrade::is_activation_height(&self.network, next_height)
630            || self
631                .network
632                .is_temporary_orchard_disabling_soft_fork_activation_height(next_height)
633        {
634            TipAction::reset_with(block)
635        } else {
636            TipAction::grow_with(block)
637        }
638    }
639
640    /// Create a new [`ChainTipChange`] from a [`LatestChainTip`] receiver and [`Network`].
641    fn new(latest_chain_tip: LatestChainTip, network: &Network) -> Self {
642        Self {
643            latest_chain_tip,
644            last_change_hash: None,
645            network: network.clone(),
646        }
647    }
648
649    /// Wait until the next chain tip change, then return the corresponding [`ChainTipBlock`].
650    ///
651    /// Returns an error if communication with the state is lost.
652    async fn tip_block_change(&mut self) -> Result<ChainTipBlock, watch::error::RecvError> {
653        loop {
654            // If there are multiple changes while this code is executing,
655            // we don't rely on getting the first block or the latest block
656            // after the change notification.
657            // Any block update after the change will do,
658            // we'll catch up with the tip after the next change.
659            self.latest_chain_tip.receiver.changed().await?;
660
661            // Wait until we have a new block
662            //
663            // last_tip_change() updates last_change_hash, but it doesn't call receiver.changed().
664            // So code that uses both sync and async methods can have spurious pending changes.
665            //
666            // TODO: use `receiver.borrow_and_update()` in `with_chain_tip_block()`,
667            //       once we upgrade to tokio 1.0 (#2200)
668            //       and remove this extra check
669            let new_block = self
670                .latest_chain_tip
671                .with_chain_tip_block(|block| {
672                    if Some(block.hash) != self.last_change_hash {
673                        Some(block.clone())
674                    } else {
675                        None
676                    }
677                })
678                .flatten();
679
680            if let Some(block) = new_block {
681                return Ok(block);
682            }
683        }
684    }
685
686    /// Returns the inner `LatestChainTip`.
687    pub fn latest_chain_tip(&self) -> LatestChainTip {
688        self.latest_chain_tip.clone()
689    }
690}
691
692impl Clone for ChainTipChange {
693    fn clone(&self) -> Self {
694        Self {
695            latest_chain_tip: self.latest_chain_tip.clone(),
696
697            // clear the previous change hash, so the first action is a reset
698            last_change_hash: None,
699
700            network: self.network.clone(),
701        }
702    }
703}
704
705impl TipAction {
706    /// Is this tip action a [`Reset`]?
707    pub fn is_reset(&self) -> bool {
708        matches!(self, Reset { .. })
709    }
710
711    /// Returns the block hash of this tip action,
712    /// regardless of the underlying variant.
713    pub fn best_tip_hash(&self) -> block::Hash {
714        match self {
715            Grow { block } => block.hash,
716            Reset { hash, .. } => *hash,
717        }
718    }
719
720    /// Returns the block height of this tip action,
721    /// regardless of the underlying variant.
722    pub fn best_tip_height(&self) -> block::Height {
723        match self {
724            Grow { block } => block.height,
725            Reset { height, .. } => *height,
726        }
727    }
728
729    /// Returns the block hash and height of this tip action,
730    /// regardless of the underlying variant.
731    pub fn best_tip_hash_and_height(&self) -> (block::Hash, block::Height) {
732        match self {
733            Grow { block } => (block.hash, block.height),
734            Reset { hash, height } => (*hash, *height),
735        }
736    }
737
738    /// Returns a [`Grow`] based on `block`.
739    pub(crate) fn grow_with(block: ChainTipBlock) -> Self {
740        Grow { block }
741    }
742
743    /// Returns a [`Reset`] based on `block`.
744    pub(crate) fn reset_with(block: ChainTipBlock) -> Self {
745        Reset {
746            height: block.height,
747            hash: block.hash,
748        }
749    }
750
751    /// Converts this [`TipAction`] into a [`Reset`].
752    ///
753    /// Designed for use in tests.
754    #[cfg(test)]
755    pub(crate) fn into_reset(self) -> Self {
756        match self {
757            Grow { block } => Reset {
758                height: block.height,
759                hash: block.hash,
760            },
761            reset @ Reset { .. } => reset,
762        }
763    }
764}