Skip to content

Commit 3915e09

Browse files
committed
feat(chain): add apply_header.. methods to LocalChain
These are convenience methods to transform a header into checkpoints to update the `LocalChain` with. Tests are included.
1 parent 96e7c45 commit 3915e09

File tree

2 files changed

+265
-2
lines changed

2 files changed

+265
-2
lines changed

crates/chain/src/local_chain.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use core::convert::Infallible;
55
use crate::collections::BTreeMap;
66
use crate::{BlockId, ChainOracle};
77
use alloc::sync::Arc;
8+
use bitcoin::block::Header;
89
use bitcoin::BlockHash;
910

1011
/// The [`ChangeSet`] represents changes to [`LocalChain`].
@@ -369,6 +370,91 @@ impl LocalChain {
369370
Ok(changeset)
370371
}
371372

373+
/// Update the chain with a given [`Header`] and a `connected_to` [`BlockId`].
374+
///
375+
/// The `header` will be transformed into checkpoints - one for the current block and one for
376+
/// the previous block. Note that a genesis header will be transformed into only one checkpoint
377+
/// (as there are no previous blocks). The checkpoints will be applied to the chain via
378+
/// [`apply_update`].
379+
///
380+
/// # Errors
381+
///
382+
/// [`ApplyHeaderError::InconsistentBlocks`] occurs if the `connected_to` block and the
383+
/// [`Header`] is inconsistent. For example, if the `connected_to` block is the same height as
384+
/// `header` or `prev_blockhash`, but has a different block hash. Or if the `connected_to`
385+
/// height is greater than the header's `height`.
386+
///
387+
/// [`ApplyHeaderError::CannotConnect`] occurs if the internal call to [`apply_update`] fails.
388+
///
389+
/// [`apply_update`]: LocalChain::apply_update
390+
pub fn apply_header_connected_to(
391+
&mut self,
392+
header: &Header,
393+
height: u32,
394+
connected_to: BlockId,
395+
) -> Result<ChangeSet, ApplyHeaderError> {
396+
let this = BlockId {
397+
height,
398+
hash: header.block_hash(),
399+
};
400+
let prev = height.checked_sub(1).map(|prev_height| BlockId {
401+
height: prev_height,
402+
hash: header.prev_blockhash,
403+
});
404+
let conn = match connected_to {
405+
// `connected_to` can be ignored if same as `this` or `prev` (duplicate)
406+
conn if conn == this || Some(conn) == prev => None,
407+
// this occurs if:
408+
// - `connected_to` height is the same as `prev`, but different hash
409+
// - `connected_to` height is the same as `this`, but different hash
410+
// - `connected_to` height is greater than `this` (this is not allowed)
411+
conn if conn.height >= height.saturating_sub(1) => {
412+
return Err(ApplyHeaderError::InconsistentBlocks)
413+
}
414+
conn => Some(conn),
415+
};
416+
417+
let update = Update {
418+
tip: CheckPoint::from_block_ids([conn, prev, Some(this)].into_iter().flatten())
419+
.expect("block ids must be in order"),
420+
introduce_older_blocks: false,
421+
};
422+
423+
self.apply_update(update)
424+
.map_err(ApplyHeaderError::CannotConnect)
425+
}
426+
427+
/// Update the chain with a given [`Header`] connecting it with the previous block.
428+
///
429+
/// This is a convenience method to call [`apply_header_connected_to`] with the `connected_to`
430+
/// parameter being `height-1:prev_blockhash`. If there is no previous block (i.e. genesis), we
431+
/// use the current block as `connected_to`.
432+
///
433+
/// [`apply_header_connected_to`]: LocalChain::apply_header_connected_to
434+
pub fn apply_header(
435+
&mut self,
436+
header: &Header,
437+
height: u32,
438+
) -> Result<ChangeSet, CannotConnectError> {
439+
let connected_to = match height.checked_sub(1) {
440+
Some(prev_height) => BlockId {
441+
height: prev_height,
442+
hash: header.prev_blockhash,
443+
},
444+
None => BlockId {
445+
height,
446+
hash: header.block_hash(),
447+
},
448+
};
449+
self.apply_header_connected_to(header, height, connected_to)
450+
.map_err(|err| match err {
451+
ApplyHeaderError::InconsistentBlocks => {
452+
unreachable!("connected_to is derived from the block so is always consistent")
453+
}
454+
ApplyHeaderError::CannotConnect(err) => err,
455+
})
456+
}
457+
372458
/// Apply the given `changeset`.
373459
pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
374460
if let Some(start_height) = changeset.keys().next().cloned() {
@@ -557,6 +643,30 @@ impl core::fmt::Display for CannotConnectError {
557643
#[cfg(feature = "std")]
558644
impl std::error::Error for CannotConnectError {}
559645

646+
/// The error type for [`LocalChain::apply_header_connected_to`].
647+
#[derive(Debug, Clone, PartialEq)]
648+
pub enum ApplyHeaderError {
649+
/// Occurs when `connected_to` block conflicts with either the current block or previous block.
650+
InconsistentBlocks,
651+
/// Occurs when the update cannot connect with the original chain.
652+
CannotConnect(CannotConnectError),
653+
}
654+
655+
impl core::fmt::Display for ApplyHeaderError {
656+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
657+
match self {
658+
ApplyHeaderError::InconsistentBlocks => write!(
659+
f,
660+
"the `connected_to` block conflicts with either the current or previous block"
661+
),
662+
ApplyHeaderError::CannotConnect(err) => core::fmt::Display::fmt(err, f),
663+
}
664+
}
665+
}
666+
667+
#[cfg(feature = "std")]
668+
impl std::error::Error for ApplyHeaderError {}
669+
560670
fn merge_chains(
561671
original_tip: CheckPoint,
562672
update_tip: CheckPoint,

crates/chain/tests/test_local_chain.rs

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use bdk_chain::{
22
local_chain::{
3-
AlterCheckPointError, CannotConnectError, ChangeSet, CheckPoint, LocalChain, Update,
3+
AlterCheckPointError, ApplyHeaderError, CannotConnectError, ChangeSet, CheckPoint,
4+
LocalChain, Update,
45
},
56
BlockId,
67
};
7-
use bitcoin::BlockHash;
8+
use bitcoin::{block::Header, hashes::Hash, BlockHash};
89

910
#[macro_use]
1011
mod common;
@@ -432,3 +433,155 @@ fn checkpoint_from_block_ids() {
432433
}
433434
}
434435
}
436+
437+
#[test]
438+
fn local_chain_apply_header_connected_to() {
439+
fn header_from_prev_blockhash(prev_blockhash: BlockHash) -> Header {
440+
Header {
441+
version: bitcoin::block::Version::default(),
442+
prev_blockhash,
443+
merkle_root: bitcoin::hash_types::TxMerkleNode::all_zeros(),
444+
time: 0,
445+
bits: bitcoin::CompactTarget::default(),
446+
nonce: 0,
447+
}
448+
}
449+
450+
struct TestCase {
451+
name: &'static str,
452+
chain: LocalChain,
453+
header: Header,
454+
height: u32,
455+
connected_to: BlockId,
456+
exp_result: Result<Vec<(u32, Option<BlockHash>)>, ApplyHeaderError>,
457+
}
458+
459+
let test_cases = [
460+
{
461+
let header = header_from_prev_blockhash(h!("A"));
462+
let hash = header.block_hash();
463+
let height = 2;
464+
let connected_to = BlockId { height, hash };
465+
TestCase {
466+
name: "connected_to_self_header_applied_to_self",
467+
chain: local_chain![(0, h!("_")), (height, hash)],
468+
header,
469+
height,
470+
connected_to,
471+
exp_result: Ok(vec![]),
472+
}
473+
},
474+
{
475+
let prev_hash = h!("A");
476+
let prev_height = 1;
477+
let header = header_from_prev_blockhash(prev_hash);
478+
let hash = header.block_hash();
479+
let height = prev_height + 1;
480+
let connected_to = BlockId {
481+
height: prev_height,
482+
hash: prev_hash,
483+
};
484+
TestCase {
485+
name: "connected_to_prev_header_applied_to_self",
486+
chain: local_chain![(0, h!("_")), (prev_height, prev_hash)],
487+
header,
488+
height,
489+
connected_to,
490+
exp_result: Ok(vec![(height, Some(hash))]),
491+
}
492+
},
493+
{
494+
let header = header_from_prev_blockhash(BlockHash::all_zeros());
495+
let hash = header.block_hash();
496+
let height = 0;
497+
let connected_to = BlockId { height, hash };
498+
TestCase {
499+
name: "genesis_applied_to_self",
500+
chain: local_chain![(0, hash)],
501+
header,
502+
height,
503+
connected_to,
504+
exp_result: Ok(vec![]),
505+
}
506+
},
507+
{
508+
let header = header_from_prev_blockhash(h!("Z"));
509+
let height = 10;
510+
let hash = header.block_hash();
511+
let prev_height = height - 1;
512+
let prev_hash = header.prev_blockhash;
513+
TestCase {
514+
name: "connect_at_connected_to",
515+
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
516+
header,
517+
height: 10,
518+
connected_to: BlockId {
519+
height: 3,
520+
hash: h!("C"),
521+
},
522+
exp_result: Ok(vec![(prev_height, Some(prev_hash)), (height, Some(hash))]),
523+
}
524+
},
525+
{
526+
let prev_hash = h!("A");
527+
let prev_height = 1;
528+
let header = header_from_prev_blockhash(prev_hash);
529+
let connected_to = BlockId {
530+
height: prev_height,
531+
hash: h!("not_prev_hash"),
532+
};
533+
TestCase {
534+
name: "inconsistent_prev_hash",
535+
chain: local_chain![(0, h!("_")), (prev_height, h!("not_prev_hash"))],
536+
header,
537+
height: prev_height + 1,
538+
connected_to,
539+
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
540+
}
541+
},
542+
{
543+
let prev_hash = h!("A");
544+
let prev_height = 1;
545+
let header = header_from_prev_blockhash(prev_hash);
546+
let height = prev_height + 1;
547+
let connected_to = BlockId {
548+
height,
549+
hash: h!("not_current_hash"),
550+
};
551+
TestCase {
552+
name: "inconsistent_current_block",
553+
chain: local_chain![(0, h!("_")), (height, h!("not_current_hash"))],
554+
header,
555+
height,
556+
connected_to,
557+
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
558+
}
559+
},
560+
{
561+
let header = header_from_prev_blockhash(h!("B"));
562+
let height = 3;
563+
let connected_to = BlockId {
564+
height: 4,
565+
hash: h!("D"),
566+
};
567+
TestCase {
568+
name: "connected_to_is_greater",
569+
chain: local_chain![(0, h!("_")), (2, h!("B"))],
570+
header,
571+
height,
572+
connected_to,
573+
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
574+
}
575+
},
576+
];
577+
578+
for (i, t) in test_cases.into_iter().enumerate() {
579+
println!("running test case {}: '{}'", i, t.name);
580+
let mut chain = t.chain;
581+
let result = chain.apply_header_connected_to(&t.header, t.height, t.connected_to);
582+
let exp_result = t
583+
.exp_result
584+
.map(|cs| cs.iter().cloned().collect::<ChangeSet>());
585+
assert_eq!(result, exp_result, "[{}:{}] unexpected result", i, t.name);
586+
}
587+
}

0 commit comments

Comments
 (0)