feat(chain): add get
and range
methods to CheckPoint
These methods allow us to query for checkpoints contained within the linked list by height and height range. This is useful to determine checkpoints to fetch for chain sources without having to refer back to the `LocalChain`. Currently this is not implemented efficiently, but in the future, we will change `CheckPoint` to use a skip list structure.
This commit is contained in:
parent
2bb654077d
commit
9a62d56900
@ -1127,18 +1127,14 @@ impl<D> Wallet<D> {
|
|||||||
// anchor tx to checkpoint with lowest height that is >= position's height
|
// anchor tx to checkpoint with lowest height that is >= position's height
|
||||||
let anchor = self
|
let anchor = self
|
||||||
.chain
|
.chain
|
||||||
.blocks()
|
|
||||||
.range(height..)
|
.range(height..)
|
||||||
.next()
|
.last()
|
||||||
.ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
|
.ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
|
||||||
tip_height: self.chain.tip().height(),
|
tip_height: self.chain.tip().height(),
|
||||||
tx_height: height,
|
tx_height: height,
|
||||||
})
|
})
|
||||||
.map(|(&anchor_height, &hash)| ConfirmationTimeHeightAnchor {
|
.map(|anchor_cp| ConfirmationTimeHeightAnchor {
|
||||||
anchor_block: BlockId {
|
anchor_block: anchor_cp.block_id(),
|
||||||
height: anchor_height,
|
|
||||||
hash,
|
|
||||||
},
|
|
||||||
confirmation_height: height,
|
confirmation_height: height,
|
||||||
confirmation_time: time,
|
confirmation_time: time,
|
||||||
})?;
|
})?;
|
||||||
|
@ -57,12 +57,15 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
local_chain.blocks(),
|
local_chain
|
||||||
&exp_hashes
|
.iter_checkpoints()
|
||||||
|
.map(|cp| (cp.height(), cp.hash()))
|
||||||
|
.collect::<BTreeSet<_>>(),
|
||||||
|
exp_hashes
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, hash)| (i as u32, *hash))
|
.map(|(i, hash)| (i as u32, *hash))
|
||||||
.collect(),
|
.collect::<BTreeSet<_>>(),
|
||||||
"final local_chain state is unexpected",
|
"final local_chain state is unexpected",
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -110,12 +113,15 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
local_chain.blocks(),
|
local_chain
|
||||||
&exp_hashes
|
.iter_checkpoints()
|
||||||
|
.map(|cp| (cp.height(), cp.hash()))
|
||||||
|
.collect::<BTreeSet<_>>(),
|
||||||
|
exp_hashes
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, hash)| (i as u32, *hash))
|
.map(|(i, hash)| (i as u32, *hash))
|
||||||
.collect(),
|
.collect::<BTreeSet<_>>(),
|
||||||
"final local_chain state is unexpected after reorg",
|
"final local_chain state is unexpected after reorg",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
//! The [`LocalChain`] is a local implementation of [`ChainOracle`].
|
//! The [`LocalChain`] is a local implementation of [`ChainOracle`].
|
||||||
|
|
||||||
use core::convert::Infallible;
|
use core::convert::Infallible;
|
||||||
|
use core::ops::RangeBounds;
|
||||||
|
|
||||||
use crate::collections::BTreeMap;
|
use crate::collections::BTreeMap;
|
||||||
use crate::{BlockId, ChainOracle};
|
use crate::{BlockId, ChainOracle};
|
||||||
use alloc::sync::Arc;
|
use alloc::sync::Arc;
|
||||||
|
use alloc::vec::Vec;
|
||||||
use bitcoin::block::Header;
|
use bitcoin::block::Header;
|
||||||
use bitcoin::BlockHash;
|
use bitcoin::BlockHash;
|
||||||
|
|
||||||
@ -148,6 +150,36 @@ impl CheckPoint {
|
|||||||
pub fn iter(&self) -> CheckPointIter {
|
pub fn iter(&self) -> CheckPointIter {
|
||||||
self.clone().into_iter()
|
self.clone().into_iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get checkpoint at `height`.
|
||||||
|
///
|
||||||
|
/// Returns `None` if checkpoint at `height` does not exist`.
|
||||||
|
pub fn get(&self, height: u32) -> Option<Self> {
|
||||||
|
self.range(height..=height).next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate checkpoints over a height range.
|
||||||
|
///
|
||||||
|
/// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
|
||||||
|
/// height).
|
||||||
|
pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint>
|
||||||
|
where
|
||||||
|
R: RangeBounds<u32>,
|
||||||
|
{
|
||||||
|
let start_bound = range.start_bound().cloned();
|
||||||
|
let end_bound = range.end_bound().cloned();
|
||||||
|
self.iter()
|
||||||
|
.skip_while(move |cp| match end_bound {
|
||||||
|
core::ops::Bound::Included(inc_bound) => cp.height() > inc_bound,
|
||||||
|
core::ops::Bound::Excluded(exc_bound) => cp.height() >= exc_bound,
|
||||||
|
core::ops::Bound::Unbounded => false,
|
||||||
|
})
|
||||||
|
.take_while(move |cp| match start_bound {
|
||||||
|
core::ops::Bound::Included(inc_bound) => cp.height() >= inc_bound,
|
||||||
|
core::ops::Bound::Excluded(exc_bound) => cp.height() > exc_bound,
|
||||||
|
core::ops::Bound::Unbounded => true,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterates over checkpoints backwards.
|
/// Iterates over checkpoints backwards.
|
||||||
@ -205,18 +237,28 @@ pub struct Update {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct LocalChain {
|
pub struct LocalChain {
|
||||||
tip: CheckPoint,
|
tip: CheckPoint,
|
||||||
index: BTreeMap<u32, BlockHash>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for LocalChain {
|
impl PartialEq for LocalChain {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.index == other.index
|
self.iter_checkpoints()
|
||||||
|
.map(|cp| cp.block_id())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
== other
|
||||||
|
.iter_checkpoints()
|
||||||
|
.map(|cp| cp.block_id())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Figure out whether we can get rid of this
|
||||||
impl From<LocalChain> for BTreeMap<u32, BlockHash> {
|
impl From<LocalChain> for BTreeMap<u32, BlockHash> {
|
||||||
fn from(value: LocalChain) -> Self {
|
fn from(value: LocalChain) -> Self {
|
||||||
value.index
|
value
|
||||||
|
.tip
|
||||||
|
.iter()
|
||||||
|
.map(|cp| (cp.height(), cp.hash()))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,18 +270,16 @@ impl ChainOracle for LocalChain {
|
|||||||
block: BlockId,
|
block: BlockId,
|
||||||
chain_tip: BlockId,
|
chain_tip: BlockId,
|
||||||
) -> Result<Option<bool>, Self::Error> {
|
) -> Result<Option<bool>, Self::Error> {
|
||||||
if block.height > chain_tip.height {
|
let chain_tip_cp = match self.tip.get(chain_tip.height) {
|
||||||
return Ok(None);
|
// we can only determine whether `block` is in chain of `chain_tip` if `chain_tip` can
|
||||||
|
// be identified in chain
|
||||||
|
Some(cp) if cp.hash() == chain_tip.hash => cp,
|
||||||
|
_ => return Ok(None),
|
||||||
|
};
|
||||||
|
match chain_tip_cp.get(block.height) {
|
||||||
|
Some(cp) => Ok(Some(cp.hash() == block.hash)),
|
||||||
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
Ok(
|
|
||||||
match (
|
|
||||||
self.index.get(&block.height),
|
|
||||||
self.index.get(&chain_tip.height),
|
|
||||||
) {
|
|
||||||
(Some(cp), Some(tip_cp)) => Some(*cp == block.hash && *tip_cp == chain_tip.hash),
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
|
fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
|
||||||
@ -250,7 +290,7 @@ impl ChainOracle for LocalChain {
|
|||||||
impl LocalChain {
|
impl LocalChain {
|
||||||
/// Get the genesis hash.
|
/// Get the genesis hash.
|
||||||
pub fn genesis_hash(&self) -> BlockHash {
|
pub fn genesis_hash(&self) -> BlockHash {
|
||||||
self.index.get(&0).copied().expect("must have genesis hash")
|
self.tip.get(0).expect("genesis must exist").hash()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct [`LocalChain`] from genesis `hash`.
|
/// Construct [`LocalChain`] from genesis `hash`.
|
||||||
@ -259,7 +299,6 @@ impl LocalChain {
|
|||||||
let height = 0;
|
let height = 0;
|
||||||
let chain = Self {
|
let chain = Self {
|
||||||
tip: CheckPoint::new(BlockId { height, hash }),
|
tip: CheckPoint::new(BlockId { height, hash }),
|
||||||
index: core::iter::once((height, hash)).collect(),
|
|
||||||
};
|
};
|
||||||
let changeset = chain.initial_changeset();
|
let changeset = chain.initial_changeset();
|
||||||
(chain, changeset)
|
(chain, changeset)
|
||||||
@ -276,7 +315,6 @@ impl LocalChain {
|
|||||||
let (mut chain, _) = Self::from_genesis_hash(genesis_hash);
|
let (mut chain, _) = Self::from_genesis_hash(genesis_hash);
|
||||||
chain.apply_changeset(&changeset)?;
|
chain.apply_changeset(&changeset)?;
|
||||||
|
|
||||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
|
||||||
debug_assert!(chain._check_changeset_is_applied(&changeset));
|
debug_assert!(chain._check_changeset_is_applied(&changeset));
|
||||||
|
|
||||||
Ok(chain)
|
Ok(chain)
|
||||||
@ -284,18 +322,11 @@ impl LocalChain {
|
|||||||
|
|
||||||
/// Construct a [`LocalChain`] from a given `checkpoint` tip.
|
/// Construct a [`LocalChain`] from a given `checkpoint` tip.
|
||||||
pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
|
pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
|
||||||
let mut chain = Self {
|
let genesis_cp = tip.iter().last().expect("must have at least one element");
|
||||||
tip,
|
if genesis_cp.height() != 0 {
|
||||||
index: BTreeMap::new(),
|
|
||||||
};
|
|
||||||
chain.reindex(0);
|
|
||||||
|
|
||||||
if chain.index.get(&0).copied().is_none() {
|
|
||||||
return Err(MissingGenesisError);
|
return Err(MissingGenesisError);
|
||||||
}
|
}
|
||||||
|
Ok(Self { tip })
|
||||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
|
||||||
Ok(chain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
|
/// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
|
||||||
@ -303,12 +334,11 @@ impl LocalChain {
|
|||||||
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
|
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
|
||||||
/// all of the same chain.
|
/// all of the same chain.
|
||||||
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Result<Self, MissingGenesisError> {
|
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Result<Self, MissingGenesisError> {
|
||||||
if !blocks.contains_key(&0) {
|
if blocks.get(&0).is_none() {
|
||||||
return Err(MissingGenesisError);
|
return Err(MissingGenesisError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut tip: Option<CheckPoint> = None;
|
let mut tip: Option<CheckPoint> = None;
|
||||||
|
|
||||||
for block in &blocks {
|
for block in &blocks {
|
||||||
match tip {
|
match tip {
|
||||||
Some(curr) => {
|
Some(curr) => {
|
||||||
@ -321,13 +351,9 @@ impl LocalChain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let chain = Self {
|
Ok(Self {
|
||||||
index: blocks,
|
|
||||||
tip: tip.expect("already checked to have genesis"),
|
tip: tip.expect("already checked to have genesis"),
|
||||||
};
|
})
|
||||||
|
|
||||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
|
||||||
Ok(chain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the highest checkpoint.
|
/// Get the highest checkpoint.
|
||||||
@ -494,9 +520,7 @@ impl LocalChain {
|
|||||||
None => LocalChain::from_blocks(extension)?.tip(),
|
None => LocalChain::from_blocks(extension)?.tip(),
|
||||||
};
|
};
|
||||||
self.tip = new_tip;
|
self.tip = new_tip;
|
||||||
self.reindex(start_height);
|
|
||||||
|
|
||||||
debug_assert!(self._check_index_is_consistent_with_tip());
|
|
||||||
debug_assert!(self._check_changeset_is_applied(changeset));
|
debug_assert!(self._check_changeset_is_applied(changeset));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -509,16 +533,16 @@ impl LocalChain {
|
|||||||
///
|
///
|
||||||
/// Replacing the block hash of an existing checkpoint will result in an error.
|
/// Replacing the block hash of an existing checkpoint will result in an error.
|
||||||
pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, AlterCheckPointError> {
|
pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, AlterCheckPointError> {
|
||||||
if let Some(&original_hash) = self.index.get(&block_id.height) {
|
if let Some(original_cp) = self.tip.get(block_id.height) {
|
||||||
|
let original_hash = original_cp.hash();
|
||||||
if original_hash != block_id.hash {
|
if original_hash != block_id.hash {
|
||||||
return Err(AlterCheckPointError {
|
return Err(AlterCheckPointError {
|
||||||
height: block_id.height,
|
height: block_id.height,
|
||||||
original_hash,
|
original_hash,
|
||||||
update_hash: Some(block_id.hash),
|
update_hash: Some(block_id.hash),
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
return Ok(ChangeSet::default());
|
|
||||||
}
|
}
|
||||||
|
return Ok(ChangeSet::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut changeset = ChangeSet::default();
|
let mut changeset = ChangeSet::default();
|
||||||
@ -542,33 +566,41 @@ impl LocalChain {
|
|||||||
/// This will fail with [`MissingGenesisError`] if the caller attempts to disconnect from the
|
/// This will fail with [`MissingGenesisError`] if the caller attempts to disconnect from the
|
||||||
/// genesis block.
|
/// genesis block.
|
||||||
pub fn disconnect_from(&mut self, block_id: BlockId) -> Result<ChangeSet, MissingGenesisError> {
|
pub fn disconnect_from(&mut self, block_id: BlockId) -> Result<ChangeSet, MissingGenesisError> {
|
||||||
if self.index.get(&block_id.height) != Some(&block_id.hash) {
|
let mut remove_from = Option::<CheckPoint>::None;
|
||||||
return Ok(ChangeSet::default());
|
let mut changeset = ChangeSet::default();
|
||||||
}
|
for cp in self.tip().iter() {
|
||||||
|
let cp_id = cp.block_id();
|
||||||
let changeset = self
|
if cp_id.height < block_id.height {
|
||||||
.index
|
|
||||||
.range(block_id.height..)
|
|
||||||
.map(|(&height, _)| (height, None))
|
|
||||||
.collect::<ChangeSet>();
|
|
||||||
self.apply_changeset(&changeset).map(|_| changeset)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reindex the heights in the chain from (and including) `from` height
|
|
||||||
fn reindex(&mut self, from: u32) {
|
|
||||||
let _ = self.index.split_off(&from);
|
|
||||||
for cp in self.iter_checkpoints() {
|
|
||||||
if cp.height() < from {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
self.index.insert(cp.height(), cp.hash());
|
changeset.insert(cp_id.height, None);
|
||||||
|
if cp_id == block_id {
|
||||||
|
remove_from = Some(cp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
self.tip = match remove_from.map(|cp| cp.prev()) {
|
||||||
|
// The checkpoint below the earliest checkpoint to remove will be the new tip.
|
||||||
|
Some(Some(new_tip)) => new_tip,
|
||||||
|
// If there is no checkpoint below the earliest checkpoint to remove, it means the
|
||||||
|
// "earliest checkpoint to remove" is the genesis block. We disallow removing the
|
||||||
|
// genesis block.
|
||||||
|
Some(None) => return Err(MissingGenesisError),
|
||||||
|
// If there is nothing to remove, we return an empty changeset.
|
||||||
|
None => return Ok(ChangeSet::default()),
|
||||||
|
};
|
||||||
|
Ok(changeset)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
|
/// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
|
||||||
/// recover the current chain.
|
/// recover the current chain.
|
||||||
pub fn initial_changeset(&self) -> ChangeSet {
|
pub fn initial_changeset(&self) -> ChangeSet {
|
||||||
self.index.iter().map(|(k, v)| (*k, Some(*v))).collect()
|
self.tip
|
||||||
|
.iter()
|
||||||
|
.map(|cp| {
|
||||||
|
let block_id = cp.block_id();
|
||||||
|
(block_id.height, Some(block_id.hash))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over checkpoints in descending height order.
|
/// Iterate over checkpoints in descending height order.
|
||||||
@ -578,28 +610,49 @@ impl LocalChain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to the internal index mapping the height to block hash.
|
|
||||||
pub fn blocks(&self) -> &BTreeMap<u32, BlockHash> {
|
|
||||||
&self.index
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _check_index_is_consistent_with_tip(&self) -> bool {
|
|
||||||
let tip_history = self
|
|
||||||
.tip
|
|
||||||
.iter()
|
|
||||||
.map(|cp| (cp.height(), cp.hash()))
|
|
||||||
.collect::<BTreeMap<_, _>>();
|
|
||||||
self.index == tip_history
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
|
fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
|
||||||
for (height, exp_hash) in changeset {
|
let mut curr_cp = self.tip.clone();
|
||||||
if self.index.get(height) != exp_hash.as_ref() {
|
for (height, exp_hash) in changeset.iter().rev() {
|
||||||
return false;
|
match curr_cp.get(*height) {
|
||||||
|
Some(query_cp) => {
|
||||||
|
if query_cp.height() != *height || Some(query_cp.hash()) != *exp_hash {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
curr_cp = query_cp;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if exp_hash.is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get checkpoint at given `height` (if it exists).
|
||||||
|
///
|
||||||
|
/// This is a shorthand for calling [`CheckPoint::get`] on the [`tip`].
|
||||||
|
///
|
||||||
|
/// [`tip`]: LocalChain::tip
|
||||||
|
pub fn get(&self, height: u32) -> Option<CheckPoint> {
|
||||||
|
self.tip.get(height)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate checkpoints over a height range.
|
||||||
|
///
|
||||||
|
/// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
|
||||||
|
/// height).
|
||||||
|
///
|
||||||
|
/// This is a shorthand for calling [`CheckPoint::range`] on the [`tip`].
|
||||||
|
///
|
||||||
|
/// [`tip`]: LocalChain::tip
|
||||||
|
pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint>
|
||||||
|
where
|
||||||
|
R: RangeBounds<u32>,
|
||||||
|
{
|
||||||
|
self.tip.range(range)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
|
/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
|
||||||
|
@ -725,13 +725,13 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
};
|
};
|
||||||
let mut has_missing_height = false;
|
let mut has_missing_height = false;
|
||||||
for anchor_block in tx_anchors.iter().map(Anchor::anchor_block) {
|
for anchor_block in tx_anchors.iter().map(Anchor::anchor_block) {
|
||||||
match chain.blocks().get(&anchor_block.height) {
|
match chain.get(anchor_block.height) {
|
||||||
None => {
|
None => {
|
||||||
has_missing_height = true;
|
has_missing_height = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Some(chain_hash) => {
|
Some(chain_cp) => {
|
||||||
if chain_hash == &anchor_block.hash {
|
if chain_cp.hash() == anchor_block.hash {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -749,7 +749,7 @@ impl<A: Anchor> TxGraph<A> {
|
|||||||
.filter_map(move |(a, _)| {
|
.filter_map(move |(a, _)| {
|
||||||
let anchor_block = a.anchor_block();
|
let anchor_block = a.anchor_block();
|
||||||
if Some(anchor_block.height) != last_height_emitted
|
if Some(anchor_block.height) != last_height_emitted
|
||||||
&& !chain.blocks().contains_key(&anchor_block.height)
|
&& chain.get(anchor_block.height).is_none()
|
||||||
{
|
{
|
||||||
last_height_emitted = Some(anchor_block.height);
|
last_height_emitted = Some(anchor_block.height);
|
||||||
Some(anchor_block.height)
|
Some(anchor_block.height)
|
||||||
@ -1299,7 +1299,7 @@ impl<A> ChangeSet<A> {
|
|||||||
A: Anchor,
|
A: Anchor,
|
||||||
{
|
{
|
||||||
self.anchor_heights()
|
self.anchor_heights()
|
||||||
.filter(move |height| !local_chain.blocks().contains_key(height))
|
.filter(move |&height| local_chain.get(height).is_none())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ use bdk_chain::{
|
|||||||
indexed_tx_graph::{self, IndexedTxGraph},
|
indexed_tx_graph::{self, IndexedTxGraph},
|
||||||
keychain::{self, Balance, KeychainTxOutIndex},
|
keychain::{self, Balance, KeychainTxOutIndex},
|
||||||
local_chain::LocalChain,
|
local_chain::LocalChain,
|
||||||
tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor,
|
tx_graph, ChainPosition, ConfirmationHeightAnchor,
|
||||||
};
|
};
|
||||||
use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut};
|
use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut};
|
||||||
use miniscript::Descriptor;
|
use miniscript::Descriptor;
|
||||||
@ -212,10 +212,8 @@ fn test_list_owned_txouts() {
|
|||||||
(
|
(
|
||||||
*tx,
|
*tx,
|
||||||
local_chain
|
local_chain
|
||||||
.blocks()
|
.get(height)
|
||||||
.get(&height)
|
.map(|cp| cp.block_id())
|
||||||
.cloned()
|
|
||||||
.map(|hash| BlockId { height, hash })
|
|
||||||
.map(|anchor_block| ConfirmationHeightAnchor {
|
.map(|anchor_block| ConfirmationHeightAnchor {
|
||||||
anchor_block,
|
anchor_block,
|
||||||
confirmation_height: anchor_block.height,
|
confirmation_height: anchor_block.height,
|
||||||
@ -230,9 +228,8 @@ fn test_list_owned_txouts() {
|
|||||||
|height: u32,
|
|height: u32,
|
||||||
graph: &IndexedTxGraph<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>| {
|
graph: &IndexedTxGraph<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>| {
|
||||||
let chain_tip = local_chain
|
let chain_tip = local_chain
|
||||||
.blocks()
|
.get(height)
|
||||||
.get(&height)
|
.map(|cp| cp.block_id())
|
||||||
.map(|&hash| BlockId { height, hash })
|
|
||||||
.unwrap_or_else(|| panic!("block must exist at {}", height));
|
.unwrap_or_else(|| panic!("block must exist at {}", height));
|
||||||
let txouts = graph
|
let txouts = graph
|
||||||
.graph()
|
.graph()
|
||||||
|
@ -528,6 +528,52 @@ fn checkpoint_from_block_ids() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn checkpoint_query() {
|
||||||
|
struct TestCase {
|
||||||
|
chain: LocalChain,
|
||||||
|
/// The heights we want to call [`CheckPoint::query`] with, represented as an inclusive
|
||||||
|
/// range.
|
||||||
|
///
|
||||||
|
/// If a [`CheckPoint`] exists at that height, we expect [`CheckPoint::query`] to return
|
||||||
|
/// it. If not, [`CheckPoint::query`] should return `None`.
|
||||||
|
query_range: (u32, u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
let test_cases = [
|
||||||
|
TestCase {
|
||||||
|
chain: local_chain![(0, h!("_")), (1, h!("A"))],
|
||||||
|
query_range: (0, 2),
|
||||||
|
},
|
||||||
|
TestCase {
|
||||||
|
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||||
|
query_range: (0, 3),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for t in test_cases.into_iter() {
|
||||||
|
let tip = t.chain.tip();
|
||||||
|
for h in t.query_range.0..=t.query_range.1 {
|
||||||
|
let query_result = tip.get(h);
|
||||||
|
|
||||||
|
// perform an exhausitive search for the checkpoint at height `h`
|
||||||
|
let exp_hash = t
|
||||||
|
.chain
|
||||||
|
.iter_checkpoints()
|
||||||
|
.find(|cp| cp.height() == h)
|
||||||
|
.map(|cp| cp.hash());
|
||||||
|
|
||||||
|
match query_result {
|
||||||
|
Some(cp) => {
|
||||||
|
assert_eq!(Some(cp.hash()), exp_hash);
|
||||||
|
assert_eq!(cp.height(), h);
|
||||||
|
}
|
||||||
|
None => assert!(exp_hash.is_none()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_chain_apply_header_connected_to() {
|
fn local_chain_apply_header_connected_to() {
|
||||||
fn header_from_prev_blockhash(prev_blockhash: BlockHash) -> Header {
|
fn header_from_prev_blockhash(prev_blockhash: BlockHash) -> Header {
|
||||||
|
@ -360,8 +360,8 @@ fn update_local_chain() -> anyhow::Result<()> {
|
|||||||
for height in t.request_heights {
|
for height in t.request_heights {
|
||||||
let exp_blockhash = blocks.get(height).expect("block must exist in bitcoind");
|
let exp_blockhash = blocks.get(height).expect("block must exist in bitcoind");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
chain.blocks().get(height),
|
chain.get(*height).map(|cp| cp.hash()),
|
||||||
Some(exp_blockhash),
|
Some(*exp_blockhash),
|
||||||
"[{}:{}] block {}:{} must exist in final chain",
|
"[{}:{}] block {}:{} must exist in final chain",
|
||||||
i,
|
i,
|
||||||
t.name,
|
t.name,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user