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 } = prepared;
119
120 Self {
121 hash,
122 height,
123 time: block.header.time,
124 transactions: block.transactions.clone(),
125 transaction_hashes,
126 previous_block_hash: block.header.previous_block_hash,
127 }
128 }
129}
130
131impl From<CheckpointVerifiedBlock> for ChainTipBlock {
132 fn from(CheckpointVerifiedBlock(prepared): CheckpointVerifiedBlock) -> Self {
133 prepared.into()
134 }
135}
136
137/// A sender for changes to the non-finalized and finalized chain tips.
138#[derive(Debug)]
139pub struct ChainTipSender {
140 /// Have we got any chain tips from the non-finalized state?
141 ///
142 /// Once this flag is set, we ignore the finalized state.
143 /// `None` tips don't set this flag.
144 use_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 use_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 use_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_use_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.use_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_use_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 // once the non-finalized state becomes active, it is always populated
216 // but ignoring `None`s makes the tests easier
217 if new_tip.is_some() {
218 self.use_non_finalized_tip = true;
219 self.update(new_tip)
220 }
221 }
222
223 /// Possibly send an update to listeners.
224 ///
225 /// An update is only sent if the current best tip is different from the last best tip
226 /// that was sent.
227 fn update(&mut self, new_tip: Option<ChainTipBlock>) {
228 // Correctness: the `self.sender.borrow()` must not be placed in a `let` binding to prevent
229 // a read-lock being created and living beyond the `self.sender.send(..)` call. If that
230 // happens, the `send` method will attempt to obtain a write-lock and will dead-lock.
231 // Without the binding, the guard is dropped at the end of the expression.
232 let active_hash = self
233 .sender
234 .borrow()
235 .as_ref()
236 .map(|active_value| active_value.hash);
237
238 let needs_update = match (new_tip.as_ref(), active_hash) {
239 // since the blocks have been contextually validated,
240 // we know their hashes cover all the block data
241 (Some(new_tip), Some(active_hash)) => new_tip.hash != active_hash,
242 (Some(_new_tip), None) => true,
243 (None, _active_value_hash) => false,
244 };
245
246 if needs_update {
247 let _ = self.sender.send(new_tip);
248 }
249 }
250
251 /// Record `new_tip` in the current span.
252 ///
253 /// Callers should create a new span with empty `new_height` and `new_hash` fields.
254 fn record_new_tip(new_tip: &Option<ChainTipBlock>) {
255 Self::record_tip(&tracing::Span::current(), "new", new_tip);
256 }
257
258 /// Record `new_tip` and the fields from `self` in the current span.
259 ///
260 /// The fields recorded are:
261 ///
262 /// - `new_height`
263 /// - `new_hash`
264 /// - `old_height`
265 /// - `old_hash`
266 /// - `old_use_non_finalized_tip`
267 ///
268 /// Callers should create a new span with the empty fields described above.
269 fn record_fields(&self, new_tip: &Option<ChainTipBlock>) {
270 let span = tracing::Span::current();
271
272 let old_tip = &*self.sender.borrow();
273
274 Self::record_tip(&span, "new", new_tip);
275 Self::record_tip(&span, "old", old_tip);
276
277 span.record(
278 "old_use_non_finalized_tip",
279 field::debug(self.use_non_finalized_tip),
280 );
281 }
282
283 /// Record `tip` into `span` using the `prefix` to name the fields.
284 ///
285 /// Callers should create a new span with empty `{prefix}_height` and `{prefix}_hash` fields.
286 fn record_tip(span: &tracing::Span, prefix: &str, tip: &Option<ChainTipBlock>) {
287 let height = tip.as_ref().map(|block| block.height);
288 let hash = tip.as_ref().map(|block| block.hash);
289
290 span.record(format!("{prefix}_height").as_str(), field::debug(height));
291 span.record(format!("{prefix}_hash").as_str(), field::debug(hash));
292 }
293}
294
295/// Efficient access to the state's current best chain tip.
296///
297/// Each method returns data from the latest tip,
298/// regardless of how many times you call it.
299///
300/// Cloned instances provide identical tip data.
301///
302/// The chain tip data is based on:
303/// * the best non-finalized chain tip, if available, or
304/// * the finalized tip.
305///
306/// ## Note
307///
308/// If a lot of blocks are committed at the same time,
309/// the latest tip will skip some blocks in the chain.
310#[derive(Clone, Debug)]
311pub struct LatestChainTip {
312 /// The receiver for the current chain tip's data.
313 receiver: WatchReceiver<ChainTipData>,
314}
315
316impl LatestChainTip {
317 /// Create a new [`LatestChainTip`] from a watch channel receiver.
318 fn new(receiver: watch::Receiver<ChainTipData>) -> Self {
319 Self {
320 receiver: WatchReceiver::new(receiver),
321 }
322 }
323
324 /// Maps the current data `ChainTipData` to `Option<U>`
325 /// by applying a function to the watched value,
326 /// while holding the receiver lock as briefly as possible.
327 ///
328 /// This helper method is a shorter way to borrow the value from the [`watch::Receiver`] and
329 /// extract some information from it, while also adding the current chain tip block's fields as
330 /// records to the current span.
331 ///
332 /// A single read lock is acquired to clone `T`, and then released after the clone.
333 /// See the performance note on [`WatchReceiver::with_watch_data`].
334 ///
335 /// Does not mark the watched data as seen.
336 ///
337 /// # Correctness
338 ///
339 /// To avoid deadlocks, see the correctness note on [`WatchReceiver::with_watch_data`].
340 fn with_chain_tip_block<U, F>(&self, f: F) -> Option<U>
341 where
342 F: FnOnce(&ChainTipBlock) -> U,
343 {
344 let span = tracing::Span::current();
345
346 let register_span_fields = |chain_tip_block: Option<&ChainTipBlock>| {
347 span.record(
348 "height",
349 tracing::field::debug(chain_tip_block.map(|block| block.height)),
350 );
351 span.record(
352 "hash",
353 tracing::field::debug(chain_tip_block.map(|block| block.hash)),
354 );
355 span.record(
356 "time",
357 tracing::field::debug(chain_tip_block.map(|block| block.time)),
358 );
359 span.record(
360 "previous_hash",
361 tracing::field::debug(chain_tip_block.map(|block| block.previous_block_hash)),
362 );
363 span.record(
364 "transaction_count",
365 tracing::field::debug(chain_tip_block.map(|block| block.transaction_hashes.len())),
366 );
367 };
368
369 self.receiver.with_watch_data(|chain_tip_block| {
370 // TODO: replace with Option::inspect when it stabilises
371 // https://github.com/rust-lang/rust/issues/91345
372 register_span_fields(chain_tip_block.as_ref());
373
374 chain_tip_block.as_ref().map(f)
375 })
376 }
377}
378
379impl ChainTip for LatestChainTip {
380 #[instrument(skip(self))]
381 fn best_tip_height(&self) -> Option<block::Height> {
382 self.with_chain_tip_block(|block| block.height)
383 }
384
385 #[instrument(skip(self))]
386 fn best_tip_hash(&self) -> Option<block::Hash> {
387 self.with_chain_tip_block(|block| block.hash)
388 }
389
390 #[instrument(skip(self))]
391 fn best_tip_height_and_hash(&self) -> Option<(block::Height, block::Hash)> {
392 self.with_chain_tip_block(|block| (block.height, block.hash))
393 }
394
395 #[instrument(skip(self))]
396 fn best_tip_block_time(&self) -> Option<DateTime<Utc>> {
397 self.with_chain_tip_block(|block| block.time)
398 }
399
400 #[instrument(skip(self))]
401 fn best_tip_height_and_block_time(&self) -> Option<(block::Height, DateTime<Utc>)> {
402 self.with_chain_tip_block(|block| (block.height, block.time))
403 }
404
405 #[instrument(skip(self))]
406 fn best_tip_mined_transaction_ids(&self) -> Arc<[transaction::Hash]> {
407 self.with_chain_tip_block(|block| block.transaction_hashes.clone())
408 .unwrap_or_else(|| Arc::new([]))
409 }
410
411 /// Returns when the state tip changes.
412 ///
413 /// Marks the state tip as seen when the returned future completes.
414 #[instrument(skip(self))]
415 async fn best_tip_changed(&mut self) -> Result<(), BoxError> {
416 self.receiver.changed().err_into().await
417 }
418
419 /// Mark the current best state tip as seen.
420 fn mark_best_tip_seen(&mut self) {
421 self.receiver.mark_as_seen();
422 }
423}
424
425/// A chain tip change monitor.
426///
427/// Awaits changes and resets of the state's best chain tip,
428/// returning the latest [`TipAction`] once the state is updated.
429///
430/// Each cloned instance separately tracks the last block data it provided. If
431/// the best chain fork has changed since the last tip change on that instance,
432/// it returns a [`Reset`].
433///
434/// The chain tip data is based on:
435/// * the best non-finalized chain tip, if available, or
436/// * the finalized tip.
437#[derive(Debug)]
438pub struct ChainTipChange {
439 /// The receiver for the current chain tip's data.
440 latest_chain_tip: LatestChainTip,
441
442 /// The most recent [`block::Hash`] provided by this instance.
443 ///
444 /// ## Note
445 ///
446 /// If the best chain fork has changed, or some blocks have been skipped,
447 /// this hash will be different to the last returned `ChainTipBlock.hash`.
448 last_change_hash: Option<block::Hash>,
449
450 /// The network for the chain tip.
451 network: Network,
452}
453
454/// Actions that we can take in response to a [`ChainTipChange`].
455#[derive(Clone, Debug, PartialEq, Eq)]
456pub enum TipAction {
457 /// The chain tip was updated continuously,
458 /// using a child `block` of the previous block.
459 ///
460 /// The genesis block action is a `Grow`.
461 Grow {
462 /// Information about the block used to grow the chain.
463 block: ChainTipBlock,
464 },
465
466 /// The chain tip was reset to a block with `height` and `hash`.
467 ///
468 /// Resets can happen for different reasons:
469 /// - a newly created or cloned [`ChainTipChange`], which is behind the
470 /// current tip,
471 /// - extending the chain with a network upgrade activation block,
472 /// - switching to a different best [`Chain`][1], also known as a rollback, and
473 /// - receiving multiple blocks since the previous change.
474 ///
475 /// To keep the code and tests simple, Zebra performs the same reset
476 /// actions, regardless of the reset reason.
477 ///
478 /// `Reset`s do not have the transaction hashes from the tip block, because
479 /// all transactions should be cleared by a reset.
480 ///
481 /// [1]: super::non_finalized_state::Chain
482 Reset {
483 /// The block height of the tip, after the chain reset.
484 height: block::Height,
485
486 /// The block hash of the tip, after the chain reset.
487 ///
488 /// Mainly useful for logging and debugging.
489 hash: block::Hash,
490 },
491}
492
493impl ChainTipChange {
494 /// Wait until the tip has changed, then return the corresponding [`TipAction`].
495 ///
496 /// The returned action describes how the tip has changed
497 /// since the last call to this method.
498 ///
499 /// If there have been no changes since the last time this method was called,
500 /// it waits for the next tip change before returning.
501 ///
502 /// If there have been multiple changes since the last time this method was called,
503 /// they are combined into a single [`TipAction::Reset`].
504 ///
505 /// Returns an error if communication with the state is lost.
506 ///
507 /// ## Note
508 ///
509 /// If a lot of blocks are committed at the same time,
510 /// the change will skip some blocks, and return a [`Reset`].
511 #[instrument(
512 skip(self),
513 fields(
514 last_change_hash = ?self.last_change_hash,
515 network = ?self.network,
516 ))]
517 pub async fn wait_for_tip_change(&mut self) -> Result<TipAction, watch::error::RecvError> {
518 let block = self.tip_block_change().await?;
519
520 let action = self.action(block.clone());
521
522 self.last_change_hash = Some(block.hash);
523
524 Ok(action)
525 }
526
527 /// Returns:
528 /// - `Some(`[`TipAction`]`)` if there has been a change since the last time the method was called.
529 /// - `None` if there has been no change.
530 ///
531 /// See [`Self::wait_for_tip_change`] for details.
532 #[instrument(
533 skip(self),
534 fields(
535 last_change_hash = ?self.last_change_hash,
536 network = ?self.network,
537 ))]
538 pub fn last_tip_change(&mut self) -> Option<TipAction> {
539 let block = self.latest_chain_tip.with_chain_tip_block(|block| {
540 if Some(block.hash) != self.last_change_hash {
541 Some(block.clone())
542 } else {
543 // Ignore an unchanged tip.
544 None
545 }
546 })??;
547
548 let block_hash = block.hash;
549 let tip_action = self.action(block);
550
551 self.last_change_hash = Some(block_hash);
552
553 Some(tip_action)
554 }
555
556 /// Sets the `last_change_hash` as the provided hash.
557 pub fn mark_last_change_hash(&mut self, hash: block::Hash) {
558 self.last_change_hash = Some(hash);
559 }
560
561 /// Return an action based on `block` and the last change we returned.
562 fn action(&self, block: ChainTipBlock) -> TipAction {
563 // check for an edge case that's dealt with by other code
564 assert!(
565 Some(block.hash) != self.last_change_hash,
566 "ChainTipSender and ChainTipChange ignore unchanged tips"
567 );
568
569 // If the previous block hash doesn't match, reset.
570 // We've either:
571 // - just initialized this instance,
572 // - changed the best chain to another fork (a rollback), or
573 // - skipped some blocks in the best chain.
574 //
575 // Consensus rules:
576 //
577 // > It is possible for a reorganization to occur
578 // > that rolls back from after the activation height, to before that height.
579 // > This can handled in the same way as any regular chain orphaning or reorganization,
580 // > as long as the new chain is valid.
581 //
582 // https://zips.z.cash/zip-0200#chain-reorganization
583
584 // If the block *before* a network upgrade activation block becomes the tip, reset.
585 //
586 // Consensus rules:
587 //
588 // > When the current chain tip height reaches ACTIVATION_HEIGHT,
589 // > the node's local transaction memory pool SHOULD be cleared of transactions
590 // > that will never be valid on the post-upgrade consensus branch.
591 //
592 // https://zips.z.cash/zip-0200#memory-pool
593 //
594 // ZIP-200 phrases this as "when the tip reaches ACTIVATION_HEIGHT", but the mempool
595 // verifies its transactions against the *next* block, i.e. the child of the current tip.
596 // A transaction in the mempool becomes invalid as soon as the next block it could be
597 // mined into is the activation block, which happens when the tip reaches
598 // `ACTIVATION_HEIGHT - 1`. So we reset when the *next* height is an activation height,
599 // one block earlier than the literal wording, so the mempool is already cleared by the
600 // time the activation block is mined on top of the tip.
601 //
602 // Skipped blocks can include network upgrade activation blocks.
603 // Fork changes can activate or deactivate a network upgrade.
604 // So we must perform the same actions for network upgrades and skipped blocks.
605 //
606 // The soft fork that temporarily disables Orchard actions is not a network upgrade, but
607 // it has the same memory-pool requirement and is verified against the next height in the
608 // same way, so we also reset when the next height is its activation height.
609 //
610 // The next height always exists because the chain tip height is far below `Height::MAX`.
611 let next_height = block
612 .height
613 .next()
614 .expect("chain tip height is far below Height::MAX");
615 if Some(block.previous_block_hash) != self.last_change_hash
616 || NetworkUpgrade::is_activation_height(&self.network, next_height)
617 || self
618 .network
619 .is_temporary_orchard_disabling_soft_fork_activation_height(next_height)
620 {
621 TipAction::reset_with(block)
622 } else {
623 TipAction::grow_with(block)
624 }
625 }
626
627 /// Create a new [`ChainTipChange`] from a [`LatestChainTip`] receiver and [`Network`].
628 fn new(latest_chain_tip: LatestChainTip, network: &Network) -> Self {
629 Self {
630 latest_chain_tip,
631 last_change_hash: None,
632 network: network.clone(),
633 }
634 }
635
636 /// Wait until the next chain tip change, then return the corresponding [`ChainTipBlock`].
637 ///
638 /// Returns an error if communication with the state is lost.
639 async fn tip_block_change(&mut self) -> Result<ChainTipBlock, watch::error::RecvError> {
640 loop {
641 // If there are multiple changes while this code is executing,
642 // we don't rely on getting the first block or the latest block
643 // after the change notification.
644 // Any block update after the change will do,
645 // we'll catch up with the tip after the next change.
646 self.latest_chain_tip.receiver.changed().await?;
647
648 // Wait until we have a new block
649 //
650 // last_tip_change() updates last_change_hash, but it doesn't call receiver.changed().
651 // So code that uses both sync and async methods can have spurious pending changes.
652 //
653 // TODO: use `receiver.borrow_and_update()` in `with_chain_tip_block()`,
654 // once we upgrade to tokio 1.0 (#2200)
655 // and remove this extra check
656 let new_block = self
657 .latest_chain_tip
658 .with_chain_tip_block(|block| {
659 if Some(block.hash) != self.last_change_hash {
660 Some(block.clone())
661 } else {
662 None
663 }
664 })
665 .flatten();
666
667 if let Some(block) = new_block {
668 return Ok(block);
669 }
670 }
671 }
672
673 /// Returns the inner `LatestChainTip`.
674 pub fn latest_chain_tip(&self) -> LatestChainTip {
675 self.latest_chain_tip.clone()
676 }
677}
678
679impl Clone for ChainTipChange {
680 fn clone(&self) -> Self {
681 Self {
682 latest_chain_tip: self.latest_chain_tip.clone(),
683
684 // clear the previous change hash, so the first action is a reset
685 last_change_hash: None,
686
687 network: self.network.clone(),
688 }
689 }
690}
691
692impl TipAction {
693 /// Is this tip action a [`Reset`]?
694 pub fn is_reset(&self) -> bool {
695 matches!(self, Reset { .. })
696 }
697
698 /// Returns the block hash of this tip action,
699 /// regardless of the underlying variant.
700 pub fn best_tip_hash(&self) -> block::Hash {
701 match self {
702 Grow { block } => block.hash,
703 Reset { hash, .. } => *hash,
704 }
705 }
706
707 /// Returns the block height of this tip action,
708 /// regardless of the underlying variant.
709 pub fn best_tip_height(&self) -> block::Height {
710 match self {
711 Grow { block } => block.height,
712 Reset { height, .. } => *height,
713 }
714 }
715
716 /// Returns the block hash and height of this tip action,
717 /// regardless of the underlying variant.
718 pub fn best_tip_hash_and_height(&self) -> (block::Hash, block::Height) {
719 match self {
720 Grow { block } => (block.hash, block.height),
721 Reset { hash, height } => (*hash, *height),
722 }
723 }
724
725 /// Returns a [`Grow`] based on `block`.
726 pub(crate) fn grow_with(block: ChainTipBlock) -> Self {
727 Grow { block }
728 }
729
730 /// Returns a [`Reset`] based on `block`.
731 pub(crate) fn reset_with(block: ChainTipBlock) -> Self {
732 Reset {
733 height: block.height,
734 hash: block.hash,
735 }
736 }
737
738 /// Converts this [`TipAction`] into a [`Reset`].
739 ///
740 /// Designed for use in tests.
741 #[cfg(test)]
742 pub(crate) fn into_reset(self) -> Self {
743 match self {
744 Grow { block } => Reset {
745 height: block.height,
746 hash: block.hash,
747 },
748 reset @ Reset { .. } => reset,
749 }
750 }
751}