Skip to content

Commit d3e5095

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 2b61a12 commit d3e5095

File tree

2 files changed

+265
-3
lines changed

2 files changed

+265
-3
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() {
@@ -579,6 +665,30 @@ impl core::fmt::Display for CannotConnectError {
579665
#[cfg(feature = "std")]
580666
impl std::error::Error for CannotConnectError {}
581667

668+
/// The error type for [`LocalChain::apply_header_connected_to`].
669+
#[derive(Debug, Clone, PartialEq)]
670+
pub enum ApplyHeaderError {
671+
/// Occurs when `connected_to` block conflicts with either the current block or previous block.
672+
InconsistentBlocks,
673+
/// Occurs when the update cannot connect with the original chain.
674+
CannotConnect(CannotConnectError),
675+
}
676+
677+
impl core::fmt::Display for ApplyHeaderError {
678+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
679+
match self {
680+
ApplyHeaderError::InconsistentBlocks => write!(
681+
f,
682+
"the `connected_to` block conflicts with either the current or previous block"
683+
),
684+
ApplyHeaderError::CannotConnect(err) => core::fmt::Display::fmt(err, f),
685+
}
686+
}
687+
}
688+
689+
#[cfg(feature = "std")]
690+
impl std::error::Error for ApplyHeaderError {}
691+
582692
fn merge_chains(
583693
original_tip: CheckPoint,
584694
update_tip: CheckPoint,

crates/chain/tests/test_local_chain.rs

Lines changed: 155 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use bdk_chain::{
22
local_chain::{
3-
AlterCheckPointError, CannotConnectError, ChangeSet, CheckPoint, LocalChain,
4-
MissingGenesisError, Update,
3+
AlterCheckPointError, ApplyHeaderError, CannotConnectError, ChangeSet, CheckPoint,
4+
LocalChain, MissingGenesisError, Update,
55
},
66
BlockId,
77
};
8-
use bitcoin::BlockHash;
8+
use bitcoin::{block::Header, hashes::Hash, BlockHash};
99

1010
#[macro_use]
1111
mod common;
@@ -506,3 +506,155 @@ fn checkpoint_from_block_ids() {
506506
}
507507
}
508508
}
509+
510+
#[test]
511+
fn local_chain_apply_header_connected_to() {
512+
fn header_from_prev_blockhash(prev_blockhash: BlockHash) -> Header {
513+
Header {
514+
version: bitcoin::block::Version::default(),
515+
prev_blockhash,
516+
merkle_root: bitcoin::hash_types::TxMerkleNode::all_zeros(),
517+
time: 0,
518+
bits: bitcoin::CompactTarget::default(),
519+
nonce: 0,
520+
}
521+
}
522+
523+
struct TestCase {
524+
name: &'static str,
525+
chain: LocalChain,
526+
header: Header,
527+
height: u32,
528+
connected_to: BlockId,
529+
exp_result: Result<Vec<(u32, Option<BlockHash>)>, ApplyHeaderError>,
530+
}
531+
532+
let test_cases = [
533+
{
534+
let header = header_from_prev_blockhash(h!("A"));
535+
let hash = header.block_hash();
536+
let height = 2;
537+
let connected_to = BlockId { height, hash };
538+
TestCase {
539+
name: "connected_to_self_header_applied_to_self",
540+
chain: local_chain![(0, h!("_")), (height, hash)],
541+
header,
542+
height,
543+
connected_to,
544+
exp_result: Ok(vec![]),
545+
}
546+
},
547+
{
548+
let prev_hash = h!("A");
549+
let prev_height = 1;
550+
let header = header_from_prev_blockhash(prev_hash);
551+
let hash = header.block_hash();
552+
let height = prev_height + 1;
553+
let connected_to = BlockId {
554+
height: prev_height,
555+
hash: prev_hash,
556+
};
557+
TestCase {
558+
name: "connected_to_prev_header_applied_to_self",
559+
chain: local_chain![(0, h!("_")), (prev_height, prev_hash)],
560+
header,
561+
height,
562+
connected_to,
563+
exp_result: Ok(vec![(height, Some(hash))]),
564+
}
565+
},
566+
{
567+
let header = header_from_prev_blockhash(BlockHash::all_zeros());
568+
let hash = header.block_hash();
569+
let height = 0;
570+
let connected_to = BlockId { height, hash };
571+
TestCase {
572+
name: "genesis_applied_to_self",
573+
chain: local_chain![(0, hash)],
574+
header,
575+
height,
576+
connected_to,
577+
exp_result: Ok(vec![]),
578+
}
579+
},
580+
{
581+
let header = header_from_prev_blockhash(h!("Z"));
582+
let height = 10;
583+
let hash = header.block_hash();
584+
let prev_height = height - 1;
585+
let prev_hash = header.prev_blockhash;
586+
TestCase {
587+
name: "connect_at_connected_to",
588+
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
589+
header,
590+
height: 10,
591+
connected_to: BlockId {
592+
height: 3,
593+
hash: h!("C"),
594+
},
595+
exp_result: Ok(vec![(prev_height, Some(prev_hash)), (height, Some(hash))]),
596+
}
597+
},
598+
{
599+
let prev_hash = h!("A");
600+
let prev_height = 1;
601+
let header = header_from_prev_blockhash(prev_hash);
602+
let connected_to = BlockId {
603+
height: prev_height,
604+
hash: h!("not_prev_hash"),
605+
};
606+
TestCase {
607+
name: "inconsistent_prev_hash",
608+
chain: local_chain![(0, h!("_")), (prev_height, h!("not_prev_hash"))],
609+
header,
610+
height: prev_height + 1,
611+
connected_to,
612+
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
613+
}
614+
},
615+
{
616+
let prev_hash = h!("A");
617+
let prev_height = 1;
618+
let header = header_from_prev_blockhash(prev_hash);
619+
let height = prev_height + 1;
620+
let connected_to = BlockId {
621+
height,
622+
hash: h!("not_current_hash"),
623+
};
624+
TestCase {
625+
name: "inconsistent_current_block",
626+
chain: local_chain![(0, h!("_")), (height, h!("not_current_hash"))],
627+
header,
628+
height,
629+
connected_to,
630+
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
631+
}
632+
},
633+
{
634+
let header = header_from_prev_blockhash(h!("B"));
635+
let height = 3;
636+
let connected_to = BlockId {
637+
height: 4,
638+
hash: h!("D"),
639+
};
640+
TestCase {
641+
name: "connected_to_is_greater",
642+
chain: local_chain![(0, h!("_")), (2, h!("B"))],
643+
header,
644+
height,
645+
connected_to,
646+
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
647+
}
648+
},
649+
];
650+
651+
for (i, t) in test_cases.into_iter().enumerate() {
652+
println!("running test case {}: '{}'", i, t.name);
653+
let mut chain = t.chain;
654+
let result = chain.apply_header_connected_to(&t.header, t.height, t.connected_to);
655+
let exp_result = t
656+
.exp_result
657+
.map(|cs| cs.iter().cloned().collect::<ChangeSet>());
658+
assert_eq!(result, exp_result, "[{}:{}] unexpected result", i, t.name);
659+
}
660+
}

0 commit comments

Comments
 (0)