1use 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
12fn 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 let mut extension = BTreeMap::default();
20 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#[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 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 pub fn genesis_hash(&self) -> BlockHash {
89 self.tip.get(0).expect("genesis must exist").hash()
90 }
91
92 #[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 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 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 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 pub fn tip(&self) -> CheckPoint {
157 self.tip.clone()
158 }
159
160 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 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 conn if conn == this || Some(conn) == prev => None,
220 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 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 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 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 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 Some(Some(new_tip)) => new_tip,
333 Some(None) => return Err(MissingGenesisError),
337 None => return Ok(ChangeSet::default()),
339 };
340 Ok(changeset)
341 }
342
343 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 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 pub fn get(&self, height: u32) -> Option<CheckPoint> {
389 self.tip.get(height)
390 }
391
392 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#[derive(Debug, Default, Clone, PartialEq)]
410#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
411pub struct ChangeSet {
412 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#[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#[derive(Clone, Debug, PartialEq)]
474pub struct AlterCheckPointError {
475 pub height: u32,
477 pub original_hash: BlockHash,
479 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#[derive(Clone, Debug, PartialEq)]
505pub struct CannotConnectError {
506 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#[derive(Debug, Clone, PartialEq)]
525pub enum ApplyHeaderError {
526 InconsistentBlocks,
528 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
547fn 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 let mut is_update_height_superset_of_original = true;
586
587 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 (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 (Some(o), u) if Some(o.height()) > u.map(|u| u.height()) => {
607 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 if u.is_none() {
617 break;
618 }
619 }
620 (Some(o), Some(u)) => {
621 if o.hash() == u.hash() {
622 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 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 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 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}