bdk_chain/
local_chain.rs

1//! The [`LocalChain`] is a local implementation of [`ChainOracle`].
2
3use core::convert::Infallible;
4use core::ops::RangeBounds;
5
6use crate::collections::BTreeMap;
7use crate::{BlockId, ChainOracle, Merge};
8pub use bdk_core::{CheckPoint, CheckPointIter};
9use bitcoin::block::Header;
10use bitcoin::BlockHash;
11
12/// Apply `changeset` to the checkpoint.
13fn apply_changeset_to_checkpoint(
14    mut init_cp: CheckPoint,
15    changeset: &ChangeSet,
16) -> Result<CheckPoint, MissingGenesisError> {
17    if let Some(start_height) = changeset.blocks.keys().next().cloned() {
18        // changes after point of agreement
19        let mut extension = BTreeMap::default();
20        // point of agreement
21        let mut base: Option<CheckPoint> = None;
22
23        for cp in init_cp.iter() {
24            if cp.height() >= start_height {
25                extension.insert(cp.height(), cp.hash());
26            } else {
27                base = Some(cp);
28                break;
29            }
30        }
31
32        for (&height, &hash) in &changeset.blocks {
33            match hash {
34                Some(hash) => {
35                    extension.insert(height, hash);
36                }
37                None => {
38                    extension.remove(&height);
39                }
40            };
41        }
42
43        let new_tip = match base {
44            Some(base) => base
45                .extend(extension.into_iter().map(BlockId::from))
46                .expect("extension is strictly greater than base"),
47            None => LocalChain::from_blocks(extension)?.tip(),
48        };
49        init_cp = new_tip;
50    }
51
52    Ok(init_cp)
53}
54
55/// This is a local implementation of [`ChainOracle`].
56#[derive(Debug, Clone, PartialEq)]
57pub struct LocalChain {
58    tip: CheckPoint,
59}
60
61impl ChainOracle for LocalChain {
62    type Error = Infallible;
63
64    fn is_block_in_chain(
65        &self,
66        block: BlockId,
67        chain_tip: BlockId,
68    ) -> Result<Option<bool>, Self::Error> {
69        let chain_tip_cp = match self.tip.get(chain_tip.height) {
70            // we can only determine whether `block` is in chain of `chain_tip` if `chain_tip` can
71            // be identified in chain
72            Some(cp) if cp.hash() == chain_tip.hash => cp,
73            _ => return Ok(None),
74        };
75        match chain_tip_cp.get(block.height) {
76            Some(cp) => Ok(Some(cp.hash() == block.hash)),
77            None => Ok(None),
78        }
79    }
80
81    fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
82        Ok(self.tip.block_id())
83    }
84}
85
86impl LocalChain {
87    /// Get the genesis hash.
88    pub fn genesis_hash(&self) -> BlockHash {
89        self.tip.get(0).expect("genesis must exist").hash()
90    }
91
92    /// Construct [`LocalChain`] from genesis `hash`.
93    #[must_use]
94    pub fn from_genesis_hash(hash: BlockHash) -> (Self, ChangeSet) {
95        let height = 0;
96        let chain = Self {
97            tip: CheckPoint::new(BlockId { height, hash }),
98        };
99        let changeset = chain.initial_changeset();
100        (chain, changeset)
101    }
102
103    /// Construct a [`LocalChain`] from an initial `changeset`.
104    pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
105        let genesis_entry = changeset.blocks.get(&0).copied().flatten();
106        let genesis_hash = match genesis_entry {
107            Some(hash) => hash,
108            None => return Err(MissingGenesisError),
109        };
110
111        let (mut chain, _) = Self::from_genesis_hash(genesis_hash);
112        chain.apply_changeset(&changeset)?;
113
114        debug_assert!(chain._check_changeset_is_applied(&changeset));
115
116        Ok(chain)
117    }
118
119    /// Construct a [`LocalChain`] from a given `checkpoint` tip.
120    pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
121        let genesis_cp = tip.iter().last().expect("must have at least one element");
122        if genesis_cp.height() != 0 {
123            return Err(MissingGenesisError);
124        }
125        Ok(Self { tip })
126    }
127
128    /// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
129    ///
130    /// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
131    /// all of the same chain.
132    pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Result<Self, MissingGenesisError> {
133        if !blocks.contains_key(&0) {
134            return Err(MissingGenesisError);
135        }
136
137        let mut tip: Option<CheckPoint> = None;
138        for block in &blocks {
139            match tip {
140                Some(curr) => {
141                    tip = Some(
142                        curr.push(BlockId::from(block))
143                            .expect("BTreeMap is ordered"),
144                    )
145                }
146                None => tip = Some(CheckPoint::new(BlockId::from(block))),
147            }
148        }
149
150        Ok(Self {
151            tip: tip.expect("already checked to have genesis"),
152        })
153    }
154
155    /// Get the highest checkpoint.
156    pub fn tip(&self) -> CheckPoint {
157        self.tip.clone()
158    }
159
160    /// Applies the given `update` to the chain.
161    ///
162    /// The method returns [`ChangeSet`] on success. This represents the changes applied to `self`.
163    ///
164    /// There must be no ambiguity about which of the existing chain's blocks are still valid and
165    /// which are now invalid. That is, the new chain must implicitly connect to a definite block in
166    /// the existing chain and invalidate the block after it (if it exists) by including a block at
167    /// the same height but with a different hash to explicitly exclude it as a connection point.
168    ///
169    /// # Errors
170    ///
171    /// An error will occur if the update does not correctly connect with `self`.
172    ///
173    /// [module-level documentation]: crate::local_chain
174    pub fn apply_update(&mut self, update: CheckPoint) -> Result<ChangeSet, CannotConnectError> {
175        let (new_tip, changeset) = merge_chains(self.tip.clone(), update)?;
176        self.tip = new_tip;
177        debug_assert!(self._check_changeset_is_applied(&changeset));
178        Ok(changeset)
179    }
180
181    /// Update the chain with a given [`Header`] at `height` which you claim is connected to a
182    /// existing block in the chain.
183    ///
184    /// This is useful when you have a block header that you want to record as part of the chain but
185    /// don't necessarily know that the `prev_blockhash` is in the chain.
186    ///
187    /// This will usually insert two new [`BlockId`]s into the chain: the header's block and the
188    /// header's `prev_blockhash` block. `connected_to` must already be in the chain but is allowed
189    /// to be `prev_blockhash` (in which case only one new block id will be inserted).
190    /// To be successful, `connected_to` must be chosen carefully so that `LocalChain`'s [update
191    /// rules][`apply_update`] are satisfied.
192    ///
193    /// # Errors
194    ///
195    /// [`ApplyHeaderError::InconsistentBlocks`] occurs if the `connected_to` block and the
196    /// [`Header`] is inconsistent. For example, if the `connected_to` block is the same height as
197    /// `header` or `prev_blockhash`, but has a different block hash. Or if the `connected_to`
198    /// height is greater than the header's `height`.
199    ///
200    /// [`ApplyHeaderError::CannotConnect`] occurs if the internal call to [`apply_update`] fails.
201    ///
202    /// [`apply_update`]: Self::apply_update
203    pub fn apply_header_connected_to(
204        &mut self,
205        header: &Header,
206        height: u32,
207        connected_to: BlockId,
208    ) -> Result<ChangeSet, ApplyHeaderError> {
209        let this = BlockId {
210            height,
211            hash: header.block_hash(),
212        };
213        let prev = height.checked_sub(1).map(|prev_height| BlockId {
214            height: prev_height,
215            hash: header.prev_blockhash,
216        });
217        let conn = match connected_to {
218            // `connected_to` can be ignored if same as `this` or `prev` (duplicate)
219            conn if conn == this || Some(conn) == prev => None,
220            // this occurs if:
221            // - `connected_to` height is the same as `prev`, but different hash
222            // - `connected_to` height is the same as `this`, but different hash
223            // - `connected_to` height is greater than `this` (this is not allowed)
224            conn if conn.height >= height.saturating_sub(1) => {
225                return Err(ApplyHeaderError::InconsistentBlocks)
226            }
227            conn => Some(conn),
228        };
229
230        let update = CheckPoint::from_block_ids([conn, prev, Some(this)].into_iter().flatten())
231            .expect("block ids must be in order");
232
233        self.apply_update(update)
234            .map_err(ApplyHeaderError::CannotConnect)
235    }
236
237    /// Update the chain with a given [`Header`] connecting it with the previous block.
238    ///
239    /// This is a convenience method to call [`apply_header_connected_to`] with the `connected_to`
240    /// parameter being `height-1:prev_blockhash`. If there is no previous block (i.e. genesis), we
241    /// use the current block as `connected_to`.
242    ///
243    /// [`apply_header_connected_to`]: LocalChain::apply_header_connected_to
244    pub fn apply_header(
245        &mut self,
246        header: &Header,
247        height: u32,
248    ) -> Result<ChangeSet, CannotConnectError> {
249        let connected_to = match height.checked_sub(1) {
250            Some(prev_height) => BlockId {
251                height: prev_height,
252                hash: header.prev_blockhash,
253            },
254            None => BlockId {
255                height,
256                hash: header.block_hash(),
257            },
258        };
259        self.apply_header_connected_to(header, height, connected_to)
260            .map_err(|err| match err {
261                ApplyHeaderError::InconsistentBlocks => {
262                    unreachable!("connected_to is derived from the block so is always consistent")
263                }
264                ApplyHeaderError::CannotConnect(err) => err,
265            })
266    }
267
268    /// Apply the given `changeset`.
269    pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
270        let old_tip = self.tip.clone();
271        let new_tip = apply_changeset_to_checkpoint(old_tip, changeset)?;
272        self.tip = new_tip;
273        debug_assert!(self._check_changeset_is_applied(changeset));
274        Ok(())
275    }
276
277    /// Insert a [`BlockId`].
278    ///
279    /// # Errors
280    ///
281    /// Replacing the block hash of an existing checkpoint will result in an error.
282    pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, AlterCheckPointError> {
283        if let Some(original_cp) = self.tip.get(block_id.height) {
284            let original_hash = original_cp.hash();
285            if original_hash != block_id.hash {
286                return Err(AlterCheckPointError {
287                    height: block_id.height,
288                    original_hash,
289                    update_hash: Some(block_id.hash),
290                });
291            }
292            return Ok(ChangeSet::default());
293        }
294
295        let mut changeset = ChangeSet::default();
296        changeset
297            .blocks
298            .insert(block_id.height, Some(block_id.hash));
299        self.apply_changeset(&changeset)
300            .map_err(|_| AlterCheckPointError {
301                height: 0,
302                original_hash: self.genesis_hash(),
303                update_hash: changeset.blocks.get(&0).cloned().flatten(),
304            })?;
305        Ok(changeset)
306    }
307
308    /// Removes blocks from (and inclusive of) the given `block_id`.
309    ///
310    /// This will remove blocks with a height equal or greater than `block_id`, but only if
311    /// `block_id` exists in the chain.
312    ///
313    /// # Errors
314    ///
315    /// This will fail with [`MissingGenesisError`] if the caller attempts to disconnect from the
316    /// genesis block.
317    pub fn disconnect_from(&mut self, block_id: BlockId) -> Result<ChangeSet, MissingGenesisError> {
318        let mut remove_from = Option::<CheckPoint>::None;
319        let mut changeset = ChangeSet::default();
320        for cp in self.tip().iter() {
321            let cp_id = cp.block_id();
322            if cp_id.height < block_id.height {
323                break;
324            }
325            changeset.blocks.insert(cp_id.height, None);
326            if cp_id == block_id {
327                remove_from = Some(cp);
328            }
329        }
330        self.tip = match remove_from.map(|cp| cp.prev()) {
331            // The checkpoint below the earliest checkpoint to remove will be the new tip.
332            Some(Some(new_tip)) => new_tip,
333            // If there is no checkpoint below the earliest checkpoint to remove, it means the
334            // "earliest checkpoint to remove" is the genesis block. We disallow removing the
335            // genesis block.
336            Some(None) => return Err(MissingGenesisError),
337            // If there is nothing to remove, we return an empty changeset.
338            None => return Ok(ChangeSet::default()),
339        };
340        Ok(changeset)
341    }
342
343    /// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
344    /// recover the current chain.
345    pub fn initial_changeset(&self) -> ChangeSet {
346        ChangeSet {
347            blocks: self
348                .tip
349                .iter()
350                .map(|cp| {
351                    let block_id = cp.block_id();
352                    (block_id.height, Some(block_id.hash))
353                })
354                .collect(),
355        }
356    }
357
358    /// Iterate over checkpoints in descending height order.
359    pub fn iter_checkpoints(&self) -> CheckPointIter {
360        self.tip.iter()
361    }
362
363    fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
364        let mut curr_cp = self.tip.clone();
365        for (height, exp_hash) in changeset.blocks.iter().rev() {
366            match curr_cp.get(*height) {
367                Some(query_cp) => {
368                    if query_cp.height() != *height || Some(query_cp.hash()) != *exp_hash {
369                        return false;
370                    }
371                    curr_cp = query_cp;
372                }
373                None => {
374                    if exp_hash.is_some() {
375                        return false;
376                    }
377                }
378            }
379        }
380        true
381    }
382
383    /// Get checkpoint at given `height` (if it exists).
384    ///
385    /// This is a shorthand for calling [`CheckPoint::get`] on the [`tip`].
386    ///
387    /// [`tip`]: LocalChain::tip
388    pub fn get(&self, height: u32) -> Option<CheckPoint> {
389        self.tip.get(height)
390    }
391
392    /// Iterate checkpoints over a height range.
393    ///
394    /// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
395    /// height).
396    ///
397    /// This is a shorthand for calling [`CheckPoint::range`] on the [`tip`].
398    ///
399    /// [`tip`]: LocalChain::tip
400    pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint>
401    where
402        R: RangeBounds<u32>,
403    {
404        self.tip.range(range)
405    }
406}
407
408/// The [`ChangeSet`] represents changes to [`LocalChain`].
409#[derive(Debug, Default, Clone, PartialEq)]
410#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
411pub struct ChangeSet {
412    /// Changes to the [`LocalChain`] blocks.
413    ///
414    /// The key represents the block height, and the value either represents added a new
415    /// [`CheckPoint`] (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
416    pub blocks: BTreeMap<u32, Option<BlockHash>>,
417}
418
419impl Merge for ChangeSet {
420    fn merge(&mut self, other: Self) {
421        Merge::merge(&mut self.blocks, other.blocks)
422    }
423
424    fn is_empty(&self) -> bool {
425        self.blocks.is_empty()
426    }
427}
428
429impl<B: IntoIterator<Item = (u32, Option<BlockHash>)>> From<B> for ChangeSet {
430    fn from(blocks: B) -> Self {
431        Self {
432            blocks: blocks.into_iter().collect(),
433        }
434    }
435}
436
437impl FromIterator<(u32, Option<BlockHash>)> for ChangeSet {
438    fn from_iter<T: IntoIterator<Item = (u32, Option<BlockHash>)>>(iter: T) -> Self {
439        Self {
440            blocks: iter.into_iter().collect(),
441        }
442    }
443}
444
445impl FromIterator<(u32, BlockHash)> for ChangeSet {
446    fn from_iter<T: IntoIterator<Item = (u32, BlockHash)>>(iter: T) -> Self {
447        Self {
448            blocks: iter
449                .into_iter()
450                .map(|(height, hash)| (height, Some(hash)))
451                .collect(),
452        }
453    }
454}
455
456/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
457#[derive(Clone, Debug, PartialEq)]
458pub struct MissingGenesisError;
459
460impl core::fmt::Display for MissingGenesisError {
461    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
462        write!(
463            f,
464            "cannot construct `LocalChain` without a genesis checkpoint"
465        )
466    }
467}
468
469#[cfg(feature = "std")]
470impl std::error::Error for MissingGenesisError {}
471
472/// Represents a failure when trying to insert/remove a checkpoint to/from [`LocalChain`].
473#[derive(Clone, Debug, PartialEq)]
474pub struct AlterCheckPointError {
475    /// The checkpoint's height.
476    pub height: u32,
477    /// The original checkpoint's block hash which cannot be replaced/removed.
478    pub original_hash: BlockHash,
479    /// The attempted update to the `original_block` hash.
480    pub update_hash: Option<BlockHash>,
481}
482
483impl core::fmt::Display for AlterCheckPointError {
484    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
485        match self.update_hash {
486            Some(update_hash) => write!(
487                f,
488                "failed to insert block at height {}: original={} update={}",
489                self.height, self.original_hash, update_hash
490            ),
491            None => write!(
492                f,
493                "failed to remove block at height {}: original={}",
494                self.height, self.original_hash
495            ),
496        }
497    }
498}
499
500#[cfg(feature = "std")]
501impl std::error::Error for AlterCheckPointError {}
502
503/// Occurs when an update does not have a common checkpoint with the original chain.
504#[derive(Clone, Debug, PartialEq)]
505pub struct CannotConnectError {
506    /// The suggested checkpoint to include to connect the two chains.
507    pub try_include_height: u32,
508}
509
510impl core::fmt::Display for CannotConnectError {
511    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
512        write!(
513            f,
514            "introduced chain cannot connect with the original chain, try include height {}",
515            self.try_include_height,
516        )
517    }
518}
519
520#[cfg(feature = "std")]
521impl std::error::Error for CannotConnectError {}
522
523/// The error type for [`LocalChain::apply_header_connected_to`].
524#[derive(Debug, Clone, PartialEq)]
525pub enum ApplyHeaderError {
526    /// Occurs when `connected_to` block conflicts with either the current block or previous block.
527    InconsistentBlocks,
528    /// Occurs when the update cannot connect with the original chain.
529    CannotConnect(CannotConnectError),
530}
531
532impl core::fmt::Display for ApplyHeaderError {
533    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
534        match self {
535            ApplyHeaderError::InconsistentBlocks => write!(
536                f,
537                "the `connected_to` block conflicts with either the current or previous block"
538            ),
539            ApplyHeaderError::CannotConnect(err) => core::fmt::Display::fmt(err, f),
540        }
541    }
542}
543
544#[cfg(feature = "std")]
545impl std::error::Error for ApplyHeaderError {}
546
547/// Applies `update_tip` onto `original_tip`.
548///
549/// On success, a tuple is returned ([`CheckPoint`], [`ChangeSet`]).
550///
551/// # Errors
552///
553/// [`CannotConnectError`] occurs when the `original_tip` and `update_tip` chains are disjoint:
554///
555/// - If no point of agreement is found between the update and original chains.
556/// - A point of agreement is found but the update is ambiguous above the point of agreement (a.k.a.
557///   the update and original chain both have a block above the point of agreement, but their
558///   heights do not overlap).
559/// - The update attempts to replace the genesis block of the original chain.
560fn merge_chains(
561    original_tip: CheckPoint,
562    update_tip: CheckPoint,
563) -> Result<(CheckPoint, ChangeSet), CannotConnectError> {
564    let mut changeset = ChangeSet::default();
565
566    let mut orig = original_tip.iter();
567    let mut update = update_tip.iter();
568
569    let mut curr_orig = None;
570    let mut curr_update = None;
571
572    let mut prev_orig: Option<CheckPoint> = None;
573    let mut prev_update: Option<CheckPoint> = None;
574
575    let mut point_of_agreement_found = false;
576
577    let mut prev_orig_was_invalidated = false;
578
579    let mut potentially_invalidated_heights = vec![];
580
581    // If we can, we want to return the update tip as the new tip because this allows checkpoints
582    // in multiple locations to keep the same `Arc` pointers when they are being updated from each
583    // other using this function. We can do this as long as the update contains every
584    // block's height of the original chain.
585    let mut is_update_height_superset_of_original = true;
586
587    // To find the difference between the new chain and the original we iterate over both of them
588    // from the tip backwards in tandem. We are always dealing with the highest one from either
589    // chain first and move to the next highest. The crucial logic is applied when they have
590    // blocks at the same height.
591    loop {
592        if curr_orig.is_none() {
593            curr_orig = orig.next();
594        }
595        if curr_update.is_none() {
596            curr_update = update.next();
597        }
598
599        match (curr_orig.as_ref(), curr_update.as_ref()) {
600            // Update block that doesn't exist in the original chain
601            (o, Some(u)) if Some(u.height()) > o.map(|o| o.height()) => {
602                changeset.blocks.insert(u.height(), Some(u.hash()));
603                prev_update = curr_update.take();
604            }
605            // Original block that isn't in the update
606            (Some(o), u) if Some(o.height()) > u.map(|u| u.height()) => {
607                // this block might be gone if an earlier block gets invalidated
608                potentially_invalidated_heights.push(o.height());
609                prev_orig_was_invalidated = false;
610                prev_orig = curr_orig.take();
611
612                is_update_height_superset_of_original = false;
613
614                // OPTIMIZATION: we have run out of update blocks so we don't need to continue
615                // iterating because there's no possibility of adding anything to changeset.
616                if u.is_none() {
617                    break;
618                }
619            }
620            (Some(o), Some(u)) => {
621                if o.hash() == u.hash() {
622                    // We have found our point of agreement 🎉 -- we require that the previous (i.e.
623                    // higher because we are iterating backwards) block in the original chain was
624                    // invalidated (if it exists). This ensures that there is an unambiguous point
625                    // of connection to the original chain from the update chain
626                    // (i.e. we know the precisely which original blocks are
627                    // invalid).
628                    if !prev_orig_was_invalidated && !point_of_agreement_found {
629                        if let (Some(prev_orig), Some(_prev_update)) = (&prev_orig, &prev_update) {
630                            return Err(CannotConnectError {
631                                try_include_height: prev_orig.height(),
632                            });
633                        }
634                    }
635                    point_of_agreement_found = true;
636                    prev_orig_was_invalidated = false;
637                    // OPTIMIZATION 2 -- if we have the same underlying pointer at this point, we
638                    // can guarantee that no older blocks are introduced.
639                    if o.eq_ptr(u) {
640                        if is_update_height_superset_of_original {
641                            return Ok((update_tip, changeset));
642                        } else {
643                            let new_tip = apply_changeset_to_checkpoint(original_tip, &changeset)
644                                .map_err(|_| CannotConnectError {
645                                try_include_height: 0,
646                            })?;
647                            return Ok((new_tip, changeset));
648                        }
649                    }
650                } else {
651                    // We have an invalidation height so we set the height to the updated hash and
652                    // also purge all the original chain block hashes above this block.
653                    changeset.blocks.insert(u.height(), Some(u.hash()));
654                    for invalidated_height in potentially_invalidated_heights.drain(..) {
655                        changeset.blocks.insert(invalidated_height, None);
656                    }
657                    prev_orig_was_invalidated = true;
658                }
659                prev_update = curr_update.take();
660                prev_orig = curr_orig.take();
661            }
662            (None, None) => {
663                break;
664            }
665            _ => {
666                unreachable!("compiler cannot tell that everything has been covered")
667            }
668        }
669    }
670
671    // When we don't have a point of agreement you can imagine it is implicitly the
672    // genesis block so we need to do the final connectivity check which in this case
673    // just means making sure the entire original chain was invalidated.
674    if !prev_orig_was_invalidated && !point_of_agreement_found {
675        if let Some(prev_orig) = prev_orig {
676            return Err(CannotConnectError {
677                try_include_height: prev_orig.height(),
678            });
679        }
680    }
681
682    let new_tip = apply_changeset_to_checkpoint(original_tip, &changeset).map_err(|_| {
683        CannotConnectError {
684            try_include_height: 0,
685        }
686    })?;
687    Ok((new_tip, changeset))
688}