Merge bitcoindevkit/bdk#1034: Implement linked-list LocalChain and update chain-src crates/examples
b206a985cffix: Even more refactoring to code and documentation (志宇)bea8e5aff4fix: `TxGraph::missing_blocks` logic (志宇)db15e03bdcfix: improve more docs and more refactoring (志宇)95312d4d05fix: docs and some minor refactoring (志宇)8bf7a997f7Refactor `debug_assertions` checks for `LocalChain` (志宇)315e7e0b4bfix: rm duplicate `bdk_tmp_plan` module (志宇)af705da1a8Add exclusion of example cli `*.db` files in `.gitignore` (志宇)eabeb6ccb1Implement linked-list `LocalChain` and update chain-src crates/examples (志宇) Pull request description: Fixes #997 Replaces #1002 ### Description This PR changes the `LocalChain` implementation to have blocks stored as a linked-list. This allows the data-src thread to hold a shared ref to a single checkpoint and have access to the whole history of checkpoints without cloning or keeping a lock on `LocalChain`. The APIs of `bdk::Wallet`, `esplora` and `electrum` are also updated to reflect these changes. Note that the `esplora` crate is rewritten to anchor txs in the confirmation block (using the esplora API's tx status block_hash). This guarantees 100% consistency between anchor blocks and their transactions (instead of anchoring txs to the latest tip). `ExploraExt` now has separate methods for updating the `TxGraph` and `LocalChain`. A new method `TxGraph::missing_blocks` is introduced for finding "floating anchors" of a `TxGraph` update (given a chain). Additional changes: * `test_local_chain.rs` is refactored to make test cases easier to write. Additional tests are also added. * Examples are updated. * Exclude example-cli `*.db` files in `.gitignore`. * Rm duplicate `bdk_tmp_plan` module. ### Notes to the reviewers This is the smallest possible division of #1002 without resulting in PRs that do not compile. Since we have changed the API of `LocalChain`, we also need to change `esplora`, `electrum` crates and examples alongside `bdk::Wallet`. ### Changelog notice * Implement linked-list `LocalChain`. This allows the data-src thread to hold a shared ref to a single checkpoint and have access to the whole history of checkpoints without cloning or keeping a lock on `LocalChain`. * Rewrote `esplora` chain-src crate to anchor txs to their confirmation blocks (using esplora API's tx-status `block_hash`). ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [x] I've added tests for the new feature * [x] I've added docs for the new feature ACKs for top commit: LLFourn: ACKb206a985cfTree-SHA512: a513eecb4f1aae6a5c06a69854e4492961424312a75a42d74377d363b364e3d52415bc81b4aa3fbc3f369ded19bddd07ab895130ebba288e8a43e9d6186e9fcc
This commit is contained in:
@@ -11,10 +11,7 @@
|
||||
//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex
|
||||
|
||||
use crate::{
|
||||
collections::BTreeMap,
|
||||
indexed_tx_graph::IndexedAdditions,
|
||||
local_chain::{self, LocalChain},
|
||||
tx_graph::TxGraph,
|
||||
collections::BTreeMap, indexed_tx_graph::IndexedAdditions, local_chain, tx_graph::TxGraph,
|
||||
Anchor, Append,
|
||||
};
|
||||
|
||||
@@ -85,24 +82,33 @@ impl<K> AsRef<BTreeMap<K, u32>> for DerivationAdditions<K> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure to update [`KeychainTxOutIndex`], [`TxGraph`] and [`LocalChain`]
|
||||
/// atomically.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
/// A structure to update [`KeychainTxOutIndex`], [`TxGraph`] and [`LocalChain`] atomically.
|
||||
///
|
||||
/// [`LocalChain`]: local_chain::LocalChain
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalUpdate<K, A> {
|
||||
/// Last active derivation index per keychain (`K`).
|
||||
pub keychain: BTreeMap<K, u32>,
|
||||
/// Contains the last active derivation indices per keychain (`K`), which is used to update the
|
||||
/// [`KeychainTxOutIndex`].
|
||||
pub last_active_indices: BTreeMap<K, u32>,
|
||||
|
||||
/// Update for the [`TxGraph`].
|
||||
pub graph: TxGraph<A>,
|
||||
|
||||
/// Update for the [`LocalChain`].
|
||||
pub chain: LocalChain,
|
||||
///
|
||||
/// [`LocalChain`]: local_chain::LocalChain
|
||||
pub chain: local_chain::Update,
|
||||
}
|
||||
|
||||
impl<K, A> Default for LocalUpdate<K, A> {
|
||||
fn default() -> Self {
|
||||
impl<K, A> LocalUpdate<K, A> {
|
||||
/// Construct a [`LocalUpdate`] with a given [`local_chain::Update`].
|
||||
///
|
||||
/// [`CheckPoint`]: local_chain::CheckPoint
|
||||
pub fn new(chain_update: local_chain::Update) -> Self {
|
||||
Self {
|
||||
keychain: Default::default(),
|
||||
graph: Default::default(),
|
||||
chain: Default::default(),
|
||||
last_active_indices: BTreeMap::new(),
|
||||
graph: TxGraph::default(),
|
||||
chain: chain_update,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,6 +128,8 @@ impl<K, A> Default for LocalUpdate<K, A> {
|
||||
)]
|
||||
pub struct LocalChangeSet<K, A> {
|
||||
/// Changes to the [`LocalChain`].
|
||||
///
|
||||
/// [`LocalChain`]: local_chain::LocalChain
|
||||
pub chain_changeset: local_chain::ChangeSet,
|
||||
|
||||
/// Additions to [`IndexedTxGraph`].
|
||||
|
||||
@@ -2,15 +2,170 @@
|
||||
|
||||
use core::convert::Infallible;
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use crate::collections::BTreeMap;
|
||||
use crate::{BlockId, ChainOracle};
|
||||
use alloc::sync::Arc;
|
||||
use bitcoin::BlockHash;
|
||||
|
||||
use crate::{BlockId, ChainOracle};
|
||||
/// A structure that represents changes to [`LocalChain`].
|
||||
///
|
||||
/// The key represents the block height, and the value either represents added a new [`CheckPoint`]
|
||||
/// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
|
||||
pub type ChangeSet = BTreeMap<u32, Option<BlockHash>>;
|
||||
|
||||
/// A [`LocalChain`] checkpoint is used to find the agreement point between two chains and as a
|
||||
/// transaction anchor.
|
||||
///
|
||||
/// Each checkpoint contains the height and hash of a block ([`BlockId`]).
|
||||
///
|
||||
/// Internaly, checkpoints are nodes of a reference-counted linked-list. This allows the caller to
|
||||
/// cheaply clone a [`CheckPoint`] without copying the whole list and to view the entire chain
|
||||
/// without holding a lock on [`LocalChain`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CheckPoint(Arc<CPInner>);
|
||||
|
||||
/// The internal contents of [`CheckPoint`].
|
||||
#[derive(Debug, Clone)]
|
||||
struct CPInner {
|
||||
/// Block id (hash and height).
|
||||
block: BlockId,
|
||||
/// Previous checkpoint (if any).
|
||||
prev: Option<Arc<CPInner>>,
|
||||
}
|
||||
|
||||
impl CheckPoint {
|
||||
/// Construct a new base block at the front of a linked list.
|
||||
pub fn new(block: BlockId) -> Self {
|
||||
Self(Arc::new(CPInner { block, prev: None }))
|
||||
}
|
||||
|
||||
/// Puts another checkpoint onto the linked list representing the blockchain.
|
||||
///
|
||||
/// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the one you
|
||||
/// are pushing on to.
|
||||
pub fn push(self, block: BlockId) -> Result<Self, Self> {
|
||||
if self.height() < block.height {
|
||||
Ok(Self(Arc::new(CPInner {
|
||||
block,
|
||||
prev: Some(self.0),
|
||||
})))
|
||||
} else {
|
||||
Err(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extends the checkpoint linked list by a iterator of block ids.
|
||||
///
|
||||
/// Returns an `Err(self)` if there is block which does not have a greater height than the
|
||||
/// previous one.
|
||||
pub fn extend(self, blocks: impl IntoIterator<Item = BlockId>) -> Result<Self, Self> {
|
||||
let mut curr = self.clone();
|
||||
for block in blocks {
|
||||
curr = curr.push(block).map_err(|_| self.clone())?;
|
||||
}
|
||||
Ok(curr)
|
||||
}
|
||||
|
||||
/// Get the [`BlockId`] of the checkpoint.
|
||||
pub fn block_id(&self) -> BlockId {
|
||||
self.0.block
|
||||
}
|
||||
|
||||
/// Get the height of the checkpoint.
|
||||
pub fn height(&self) -> u32 {
|
||||
self.0.block.height
|
||||
}
|
||||
|
||||
/// Get the block hash of the checkpoint.
|
||||
pub fn hash(&self) -> BlockHash {
|
||||
self.0.block.hash
|
||||
}
|
||||
|
||||
/// Get the previous checkpoint in the chain
|
||||
pub fn prev(&self) -> Option<CheckPoint> {
|
||||
self.0.prev.clone().map(CheckPoint)
|
||||
}
|
||||
|
||||
/// Iterate from this checkpoint in descending height.
|
||||
pub fn iter(&self) -> CheckPointIter {
|
||||
self.clone().into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// A structure that iterates over checkpoints backwards.
|
||||
pub struct CheckPointIter {
|
||||
current: Option<Arc<CPInner>>,
|
||||
}
|
||||
|
||||
impl Iterator for CheckPointIter {
|
||||
type Item = CheckPoint;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let current = self.current.clone()?;
|
||||
self.current = current.prev.clone();
|
||||
Some(CheckPoint(current))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for CheckPoint {
|
||||
type Item = CheckPoint;
|
||||
type IntoIter = CheckPointIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
CheckPointIter {
|
||||
current: Some(self.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct to update [`LocalChain`].
|
||||
///
|
||||
/// This is used as input for [`LocalChain::apply_update`]. It contains the update's chain `tip` and
|
||||
/// a flag `introduce_older_blocks` which signals whether this update intends to introduce missing
|
||||
/// blocks to the original chain.
|
||||
///
|
||||
/// Block-by-block syncing mechanisms would typically create updates that builds upon the previous
|
||||
/// tip. In this case, `introduce_older_blocks` would be `false`.
|
||||
///
|
||||
/// Script-pubkey based syncing mechanisms may not introduce transactions in a chronological order
|
||||
/// so some updates require introducing older blocks (to anchor older transactions). For
|
||||
/// script-pubkey based syncing, `introduce_older_blocks` would typically be `true`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Update {
|
||||
/// The update chain's new tip.
|
||||
pub tip: CheckPoint,
|
||||
|
||||
/// Whether the update allows for introducing older blocks.
|
||||
///
|
||||
/// Refer to [struct-level documentation] for more.
|
||||
///
|
||||
/// [struct-level documentation]: Update
|
||||
pub introduce_older_blocks: bool,
|
||||
}
|
||||
|
||||
/// This is a local implementation of [`ChainOracle`].
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct LocalChain {
|
||||
blocks: BTreeMap<u32, BlockHash>,
|
||||
tip: Option<CheckPoint>,
|
||||
index: BTreeMap<u32, BlockHash>,
|
||||
}
|
||||
|
||||
impl PartialEq for LocalChain {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.index == other.index
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalChain> for BTreeMap<u32, BlockHash> {
|
||||
fn from(value: LocalChain) -> Self {
|
||||
value.index
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BTreeMap<u32, BlockHash>> for LocalChain {
|
||||
fn from(value: BTreeMap<u32, BlockHash>) -> Self {
|
||||
Self::from_blocks(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainOracle for LocalChain {
|
||||
@@ -19,215 +174,247 @@ impl ChainOracle for LocalChain {
|
||||
fn is_block_in_chain(
|
||||
&self,
|
||||
block: BlockId,
|
||||
static_block: BlockId,
|
||||
chain_tip: BlockId,
|
||||
) -> Result<Option<bool>, Self::Error> {
|
||||
if block.height > static_block.height {
|
||||
if block.height > chain_tip.height {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(
|
||||
match (
|
||||
self.blocks.get(&block.height),
|
||||
self.blocks.get(&static_block.height),
|
||||
self.index.get(&block.height),
|
||||
self.index.get(&chain_tip.height),
|
||||
) {
|
||||
(Some(&hash), Some(&static_hash)) => {
|
||||
Some(hash == block.hash && static_hash == static_block.hash)
|
||||
}
|
||||
(Some(cp), Some(tip_cp)) => Some(*cp == block.hash && *tip_cp == chain_tip.hash),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn get_chain_tip(&self) -> Result<Option<BlockId>, Self::Error> {
|
||||
Ok(self.tip())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<BTreeMap<u32, BlockHash>> for LocalChain {
|
||||
fn as_ref(&self) -> &BTreeMap<u32, BlockHash> {
|
||||
&self.blocks
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalChain> for BTreeMap<u32, BlockHash> {
|
||||
fn from(value: LocalChain) -> Self {
|
||||
value.blocks
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BTreeMap<u32, BlockHash>> for LocalChain {
|
||||
fn from(value: BTreeMap<u32, BlockHash>) -> Self {
|
||||
Self { blocks: value }
|
||||
Ok(self.tip.as_ref().map(|tip| tip.block_id()))
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalChain {
|
||||
/// Contruct a [`LocalChain`] from a list of [`BlockId`]s.
|
||||
pub fn from_blocks<B>(blocks: B) -> Self
|
||||
where
|
||||
B: IntoIterator<Item = BlockId>,
|
||||
{
|
||||
Self {
|
||||
blocks: blocks.into_iter().map(|b| (b.height, b.hash)).collect(),
|
||||
}
|
||||
/// Construct a [`LocalChain`] from an initial `changeset`.
|
||||
pub fn from_changeset(changeset: ChangeSet) -> Self {
|
||||
let mut chain = Self::default();
|
||||
chain.apply_changeset(&changeset);
|
||||
|
||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
||||
debug_assert!(chain._check_changeset_is_applied(&changeset));
|
||||
|
||||
chain
|
||||
}
|
||||
|
||||
/// Get a reference to a map of block height to hash.
|
||||
pub fn blocks(&self) -> &BTreeMap<u32, BlockHash> {
|
||||
&self.blocks
|
||||
}
|
||||
|
||||
/// Get the chain tip.
|
||||
pub fn tip(&self) -> Option<BlockId> {
|
||||
self.blocks
|
||||
.iter()
|
||||
.last()
|
||||
.map(|(&height, &hash)| BlockId { height, hash })
|
||||
}
|
||||
|
||||
/// This is like the sparsechain's logic, expect we must guarantee that all invalidated heights
|
||||
/// are to be re-filled.
|
||||
pub fn determine_changeset(&self, update: &Self) -> Result<ChangeSet, UpdateNotConnectedError> {
|
||||
let update = update.as_ref();
|
||||
let update_tip = match update.keys().last().cloned() {
|
||||
Some(tip) => tip,
|
||||
None => return Ok(ChangeSet::default()),
|
||||
/// Construct a [`LocalChain`] from a given `checkpoint` tip.
|
||||
pub fn from_tip(tip: CheckPoint) -> Self {
|
||||
let mut chain = Self {
|
||||
tip: Some(tip),
|
||||
..Default::default()
|
||||
};
|
||||
chain.reindex(0);
|
||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
||||
chain
|
||||
}
|
||||
|
||||
// this is the latest height where both the update and local chain has the same block hash
|
||||
let agreement_height = update
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|&(u_height, u_hash)| self.blocks.get(u_height) == Some(u_hash))
|
||||
.map(|(&height, _)| height);
|
||||
/// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
|
||||
///
|
||||
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
|
||||
/// all of the same chain.
|
||||
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Self {
|
||||
let mut tip: Option<CheckPoint> = None;
|
||||
|
||||
// the lower bound of the range to invalidate
|
||||
let invalidate_lb = match agreement_height {
|
||||
Some(height) if height == update_tip => u32::MAX,
|
||||
Some(height) => height + 1,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
// the first block's height to invalidate in the local chain
|
||||
let invalidate_from_height = self.blocks.range(invalidate_lb..).next().map(|(&h, _)| h);
|
||||
|
||||
// the first block of height to invalidate (if any) should be represented in the update
|
||||
if let Some(first_invalid_height) = invalidate_from_height {
|
||||
if !update.contains_key(&first_invalid_height) {
|
||||
return Err(UpdateNotConnectedError(first_invalid_height));
|
||||
}
|
||||
}
|
||||
|
||||
let mut changeset: BTreeMap<u32, Option<BlockHash>> = match invalidate_from_height {
|
||||
Some(first_invalid_height) => {
|
||||
// the first block of height to invalidate should be represented in the update
|
||||
if !update.contains_key(&first_invalid_height) {
|
||||
return Err(UpdateNotConnectedError(first_invalid_height));
|
||||
for block in &blocks {
|
||||
match tip {
|
||||
Some(curr) => {
|
||||
tip = Some(
|
||||
curr.push(BlockId::from(block))
|
||||
.expect("BTreeMap is ordered"),
|
||||
)
|
||||
}
|
||||
self.blocks
|
||||
.range(first_invalid_height..)
|
||||
.map(|(height, _)| (*height, None))
|
||||
.collect()
|
||||
}
|
||||
None => BTreeMap::new(),
|
||||
};
|
||||
for (height, update_hash) in update {
|
||||
let original_hash = self.blocks.get(height);
|
||||
if Some(update_hash) != original_hash {
|
||||
changeset.insert(*height, Some(*update_hash));
|
||||
None => tip = Some(CheckPoint::new(BlockId::from(block))),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changeset)
|
||||
let chain = Self { index: blocks, tip };
|
||||
|
||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
||||
|
||||
chain
|
||||
}
|
||||
|
||||
/// Applies the given `changeset`.
|
||||
pub fn apply_changeset(&mut self, changeset: ChangeSet) {
|
||||
for (height, blockhash) in changeset {
|
||||
match blockhash {
|
||||
Some(blockhash) => self.blocks.insert(height, blockhash),
|
||||
None => self.blocks.remove(&height),
|
||||
/// Get the highest checkpoint.
|
||||
pub fn tip(&self) -> Option<CheckPoint> {
|
||||
self.tip.clone()
|
||||
}
|
||||
|
||||
/// Returns whether the [`LocalChain`] is empty (has no checkpoints).
|
||||
pub fn is_empty(&self) -> bool {
|
||||
let res = self.tip.is_none();
|
||||
debug_assert_eq!(res, self.index.is_empty());
|
||||
res
|
||||
}
|
||||
|
||||
/// Applies the given `update` to the chain.
|
||||
///
|
||||
/// The method returns [`ChangeSet`] on success. This represents the applied changes to `self`.
|
||||
///
|
||||
/// There must be no ambiguity about which of the existing chain's blocks are still valid and
|
||||
/// which are now invalid. That is, the new chain must implicitly connect to a definite block in
|
||||
/// the existing chain and invalidate the block after it (if it exists) by including a block at
|
||||
/// the same height but with a different hash to explicitly exclude it as a connection point.
|
||||
///
|
||||
/// Additionally, an empty chain can be updated with any chain, and a chain with a single block
|
||||
/// can have it's block invalidated by an update chain with a block at the same height but
|
||||
/// different hash.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error will occur if the update does not correctly connect with `self`.
|
||||
///
|
||||
/// Refer to [`Update`] for more about the update struct.
|
||||
///
|
||||
/// [module-level documentation]: crate::local_chain
|
||||
pub fn apply_update(&mut self, update: Update) -> Result<ChangeSet, CannotConnectError> {
|
||||
match self.tip() {
|
||||
Some(original_tip) => {
|
||||
let changeset = merge_chains(
|
||||
original_tip,
|
||||
update.tip.clone(),
|
||||
update.introduce_older_blocks,
|
||||
)?;
|
||||
self.apply_changeset(&changeset);
|
||||
|
||||
// return early as `apply_changeset` already calls `check_consistency`
|
||||
Ok(changeset)
|
||||
}
|
||||
None => {
|
||||
*self = Self::from_tip(update.tip);
|
||||
let changeset = self.initial_changeset();
|
||||
|
||||
debug_assert!(self._check_index_is_consistent_with_tip());
|
||||
debug_assert!(self._check_changeset_is_applied(&changeset));
|
||||
Ok(changeset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the given `changeset`.
|
||||
pub fn apply_changeset(&mut self, changeset: &ChangeSet) {
|
||||
if let Some(start_height) = changeset.keys().next().cloned() {
|
||||
let mut extension = BTreeMap::default();
|
||||
let mut base: Option<CheckPoint> = None;
|
||||
for cp in self.iter_checkpoints() {
|
||||
if cp.height() >= start_height {
|
||||
extension.insert(cp.height(), cp.hash());
|
||||
} else {
|
||||
base = Some(cp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (&height, &hash) in changeset {
|
||||
match hash {
|
||||
Some(hash) => {
|
||||
extension.insert(height, hash);
|
||||
}
|
||||
None => {
|
||||
extension.remove(&height);
|
||||
}
|
||||
};
|
||||
}
|
||||
let new_tip = match base {
|
||||
Some(base) => Some(
|
||||
base.extend(extension.into_iter().map(BlockId::from))
|
||||
.expect("extension is strictly greater than base"),
|
||||
),
|
||||
None => LocalChain::from_blocks(extension).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));
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates [`LocalChain`] with an update [`LocalChain`].
|
||||
/// Insert a [`BlockId`].
|
||||
///
|
||||
/// This is equivalent to calling [`determine_changeset`] and [`apply_changeset`] in sequence.
|
||||
/// # Errors
|
||||
///
|
||||
/// [`determine_changeset`]: Self::determine_changeset
|
||||
/// [`apply_changeset`]: Self::apply_changeset
|
||||
pub fn apply_update(&mut self, update: Self) -> Result<ChangeSet, UpdateNotConnectedError> {
|
||||
let changeset = self.determine_changeset(&update)?;
|
||||
self.apply_changeset(changeset.clone());
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Derives a [`ChangeSet`] that assumes that there are no preceding changesets.
|
||||
///
|
||||
/// The changeset returned will record additions of all blocks included in [`Self`].
|
||||
pub fn initial_changeset(&self) -> ChangeSet {
|
||||
self.blocks
|
||||
.iter()
|
||||
.map(|(&height, &hash)| (height, Some(hash)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Insert a block of [`BlockId`] into the [`LocalChain`].
|
||||
///
|
||||
/// # Error
|
||||
///
|
||||
/// If the insertion height already contains a block, and the block has a different blockhash,
|
||||
/// this will result in an [`InsertBlockNotMatchingError`].
|
||||
pub fn insert_block(
|
||||
&mut self,
|
||||
block_id: BlockId,
|
||||
) -> Result<ChangeSet, InsertBlockNotMatchingError> {
|
||||
let mut update = Self::from_blocks(self.tip());
|
||||
|
||||
if let Some(original_hash) = update.blocks.insert(block_id.height, block_id.hash) {
|
||||
/// Replacing the block hash of an existing checkpoint will result in an error.
|
||||
pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, InsertBlockError> {
|
||||
if let Some(&original_hash) = self.index.get(&block_id.height) {
|
||||
if original_hash != block_id.hash {
|
||||
return Err(InsertBlockNotMatchingError {
|
||||
return Err(InsertBlockError {
|
||||
height: block_id.height,
|
||||
original_hash,
|
||||
update_hash: block_id.hash,
|
||||
});
|
||||
} else {
|
||||
return Ok(ChangeSet::default());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.apply_update(update).expect("should always connect"))
|
||||
let mut changeset = ChangeSet::default();
|
||||
changeset.insert(block_id.height, Some(block_id.hash));
|
||||
self.apply_changeset(&changeset);
|
||||
Ok(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;
|
||||
}
|
||||
self.index.insert(cp.height(), cp.hash());
|
||||
}
|
||||
}
|
||||
|
||||
/// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
|
||||
/// recover the current chain.
|
||||
pub fn initial_changeset(&self) -> ChangeSet {
|
||||
self.index.iter().map(|(k, v)| (*k, Some(*v))).collect()
|
||||
}
|
||||
|
||||
/// Iterate over checkpoints in descending height order.
|
||||
pub fn iter_checkpoints(&self) -> CheckPointIter {
|
||||
CheckPointIter {
|
||||
current: self.tip.as_ref().map(|tip| tip.0.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
.flat_map(CheckPoint::iter)
|
||||
.map(|cp| (cp.height(), cp.hash()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
self.index == tip_history
|
||||
}
|
||||
|
||||
fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
|
||||
for (height, exp_hash) in changeset {
|
||||
if self.index.get(height) != exp_hash.as_ref() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`].
|
||||
///
|
||||
/// [`determine_changeset`]: LocalChain::determine_changeset
|
||||
pub type ChangeSet = BTreeMap<u32, Option<BlockHash>>;
|
||||
|
||||
/// Represents an update failure of [`LocalChain`] due to the update not connecting to the original
|
||||
/// chain.
|
||||
///
|
||||
/// The update cannot be applied to the chain because the chain suffix it represents did not
|
||||
/// connect to the existing chain. This error case contains the checkpoint height to include so
|
||||
/// that the chains can connect.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct UpdateNotConnectedError(pub u32);
|
||||
|
||||
impl core::fmt::Display for UpdateNotConnectedError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"the update cannot connect with the chain, try include block at height {}",
|
||||
self.0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for UpdateNotConnectedError {}
|
||||
|
||||
/// Represents a failure when trying to insert a checkpoint into [`LocalChain`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct InsertBlockNotMatchingError {
|
||||
pub struct InsertBlockError {
|
||||
/// The checkpoints' height.
|
||||
pub height: u32,
|
||||
/// Original checkpoint's block hash.
|
||||
@@ -236,7 +423,7 @@ pub struct InsertBlockNotMatchingError {
|
||||
pub update_hash: BlockHash,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for InsertBlockNotMatchingError {
|
||||
impl core::fmt::Display for InsertBlockError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
@@ -247,4 +434,129 @@ impl core::fmt::Display for InsertBlockNotMatchingError {
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for InsertBlockNotMatchingError {}
|
||||
impl std::error::Error for InsertBlockError {}
|
||||
|
||||
/// Occurs when an update does not have a common checkpoint with the original chain.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CannotConnectError {
|
||||
/// The suggested checkpoint to include to connect the two chains.
|
||||
pub try_include_height: u32,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for CannotConnectError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"introduced chain cannot connect with the original chain, try include height {}",
|
||||
self.try_include_height,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for CannotConnectError {}
|
||||
|
||||
fn merge_chains(
|
||||
original_tip: CheckPoint,
|
||||
update_tip: CheckPoint,
|
||||
introduce_older_blocks: bool,
|
||||
) -> Result<ChangeSet, CannotConnectError> {
|
||||
let mut changeset = ChangeSet::default();
|
||||
let mut orig = original_tip.into_iter();
|
||||
let mut update = update_tip.into_iter();
|
||||
let mut curr_orig = None;
|
||||
let mut curr_update = None;
|
||||
let mut prev_orig: Option<CheckPoint> = None;
|
||||
let mut prev_update: Option<CheckPoint> = None;
|
||||
let mut point_of_agreement_found = false;
|
||||
let mut prev_orig_was_invalidated = false;
|
||||
let mut potentially_invalidated_heights = vec![];
|
||||
|
||||
// To find the difference between the new chain and the original we iterate over both of them
|
||||
// from the tip backwards in tandem. We always dealing with the highest one from either chain
|
||||
// first and move to the next highest. The crucial logic is applied when they have blocks at the
|
||||
// same height.
|
||||
loop {
|
||||
if curr_orig.is_none() {
|
||||
curr_orig = orig.next();
|
||||
}
|
||||
if curr_update.is_none() {
|
||||
curr_update = update.next();
|
||||
}
|
||||
|
||||
match (curr_orig.as_ref(), curr_update.as_ref()) {
|
||||
// Update block that doesn't exist in the original chain
|
||||
(o, Some(u)) if Some(u.height()) > o.map(|o| o.height()) => {
|
||||
changeset.insert(u.height(), Some(u.hash()));
|
||||
prev_update = curr_update.take();
|
||||
}
|
||||
// Original block that isn't in the update
|
||||
(Some(o), u) if Some(o.height()) > u.map(|u| u.height()) => {
|
||||
// this block might be gone if an earlier block gets invalidated
|
||||
potentially_invalidated_heights.push(o.height());
|
||||
prev_orig_was_invalidated = false;
|
||||
prev_orig = curr_orig.take();
|
||||
|
||||
// OPTIMIZATION: we have run out of update blocks so we don't need to continue
|
||||
// iterating becuase there's no possibility of adding anything to changeset.
|
||||
if u.is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
(Some(o), Some(u)) => {
|
||||
if o.hash() == u.hash() {
|
||||
// We have found our point of agreement 🎉 -- we require that the previous (i.e.
|
||||
// higher because we are iterating backwards) block in the original chain was
|
||||
// invalidated (if it exists). This ensures that there is an unambigious point of
|
||||
// connection to the original chain from the update chain (i.e. we know the
|
||||
// precisely which original blocks are invalid).
|
||||
if !prev_orig_was_invalidated && !point_of_agreement_found {
|
||||
if let (Some(prev_orig), Some(_prev_update)) = (&prev_orig, &prev_update) {
|
||||
return Err(CannotConnectError {
|
||||
try_include_height: prev_orig.height(),
|
||||
});
|
||||
}
|
||||
}
|
||||
point_of_agreement_found = true;
|
||||
prev_orig_was_invalidated = false;
|
||||
// OPTIMIZATION 1 -- If we know that older blocks cannot be introduced without
|
||||
// invalidation, we can break after finding the point of agreement.
|
||||
// OPTIMIZATION 2 -- if we have the same underlying pointer at this point, we
|
||||
// can guarantee that no older blocks are introduced.
|
||||
if !introduce_older_blocks || Arc::as_ptr(&o.0) == Arc::as_ptr(&u.0) {
|
||||
return Ok(changeset);
|
||||
}
|
||||
} else {
|
||||
// We have an invalidation height so we set the height to the updated hash and
|
||||
// also purge all the original chain block hashes above this block.
|
||||
changeset.insert(u.height(), Some(u.hash()));
|
||||
for invalidated_height in potentially_invalidated_heights.drain(..) {
|
||||
changeset.insert(invalidated_height, None);
|
||||
}
|
||||
prev_orig_was_invalidated = true;
|
||||
}
|
||||
prev_update = curr_update.take();
|
||||
prev_orig = curr_orig.take();
|
||||
}
|
||||
(None, None) => {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
unreachable!("compiler cannot tell that everything has been covered")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When we don't have a point of agreement you can imagine it is implicitly the
|
||||
// genesis block so we need to do the final connectivity check which in this case
|
||||
// just means making sure the entire original chain was invalidated.
|
||||
if !prev_orig_was_invalidated && !point_of_agreement_found {
|
||||
if let Some(prev_orig) = prev_orig {
|
||||
return Err(CannotConnectError {
|
||||
try_include_height: prev_orig.height(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ impl ForEachTxOut for Transaction {
|
||||
|
||||
/// Trait that "anchors" blockchain data to a specific block of height and hash.
|
||||
///
|
||||
/// [`Anchor`] implementations must be [`Ord`] by the anchor block's [`BlockId`] first.
|
||||
///
|
||||
/// I.e. If transaction A is anchored in block B, then if block B is in the best chain, we can
|
||||
/// assume that transaction A is also confirmed in the best chain. This does not necessarily mean
|
||||
/// that transaction A is confirmed in block B. It could also mean transaction A is confirmed in a
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
//! ```
|
||||
|
||||
use crate::{
|
||||
collections::*, keychain::Balance, Anchor, Append, BlockId, ChainOracle, ChainPosition,
|
||||
ForEachTxOut, FullTxOut,
|
||||
collections::*, keychain::Balance, local_chain::LocalChain, Anchor, Append, BlockId,
|
||||
ChainOracle, ChainPosition, ForEachTxOut, FullTxOut,
|
||||
};
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
|
||||
@@ -598,6 +598,69 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
}
|
||||
|
||||
impl<A: Anchor> TxGraph<A> {
|
||||
/// Find missing block heights of `chain`.
|
||||
///
|
||||
/// This works by scanning through anchors, and seeing whether the anchor block of the anchor
|
||||
/// exists in the [`LocalChain`]. The returned iterator does not output duplicate heights.
|
||||
pub fn missing_heights<'a>(&'a self, chain: &'a LocalChain) -> impl Iterator<Item = u32> + 'a {
|
||||
// Map of txids to skip.
|
||||
//
|
||||
// Usually, if a height of a tx anchor is missing from the chain, we would want to return
|
||||
// this height in the iterator. The exception is when the tx is confirmed in chain. All the
|
||||
// other missing-height anchors of this tx can be skipped.
|
||||
//
|
||||
// * Some(true) => skip all anchors of this txid
|
||||
// * Some(false) => do not skip anchors of this txid
|
||||
// * None => we do not know whether we can skip this txid
|
||||
let mut txids_to_skip = HashMap::<Txid, bool>::new();
|
||||
|
||||
// Keeps track of the last height emitted so we don't double up.
|
||||
let mut last_height_emitted = Option::<u32>::None;
|
||||
|
||||
self.anchors
|
||||
.iter()
|
||||
.filter(move |(_, txid)| {
|
||||
let skip = *txids_to_skip.entry(*txid).or_insert_with(|| {
|
||||
let tx_anchors = match self.txs.get(txid) {
|
||||
Some((_, anchors, _)) => anchors,
|
||||
None => return true,
|
||||
};
|
||||
let mut has_missing_height = false;
|
||||
for anchor_block in tx_anchors.iter().map(Anchor::anchor_block) {
|
||||
match chain.blocks().get(&anchor_block.height) {
|
||||
None => {
|
||||
has_missing_height = true;
|
||||
continue;
|
||||
}
|
||||
Some(chain_hash) => {
|
||||
if chain_hash == &anchor_block.hash {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
!has_missing_height
|
||||
});
|
||||
#[cfg(feature = "std")]
|
||||
debug_assert!({
|
||||
println!("txid={} skip={}", txid, skip);
|
||||
true
|
||||
});
|
||||
!skip
|
||||
})
|
||||
.filter_map(move |(a, _)| {
|
||||
let anchor_block = a.anchor_block();
|
||||
if Some(anchor_block.height) != last_height_emitted
|
||||
&& !chain.blocks().contains_key(&anchor_block.height)
|
||||
{
|
||||
last_height_emitted = Some(anchor_block.height);
|
||||
Some(anchor_block.height)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the position of the transaction in `chain` with tip `chain_tip`.
|
||||
///
|
||||
/// If the given transaction of `txid` does not exist in the chain of `chain_tip`, `None` is
|
||||
|
||||
@@ -9,25 +9,20 @@ macro_rules! h {
|
||||
macro_rules! local_chain {
|
||||
[ $(($height:expr, $block_hash:expr)), * ] => {{
|
||||
#[allow(unused_mut)]
|
||||
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*])
|
||||
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect())
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! chain {
|
||||
($([$($tt:tt)*]),*) => { chain!( checkpoints: [$([$($tt)*]),*] ) };
|
||||
(checkpoints: $($tail:tt)*) => { chain!( index: TxHeight, checkpoints: $($tail)*) };
|
||||
(index: $ind:ty, checkpoints: [ $([$height:expr, $block_hash:expr]),* ] $(,txids: [$(($txid:expr, $tx_height:expr)),*])?) => {{
|
||||
macro_rules! chain_update {
|
||||
[ $(($height:expr, $hash:expr)), * ] => {{
|
||||
#[allow(unused_mut)]
|
||||
let mut chain = bdk_chain::sparse_chain::SparseChain::<$ind>::from_checkpoints([$(($height, $block_hash).into()),*]);
|
||||
|
||||
$(
|
||||
$(
|
||||
let _ = chain.insert_tx($txid, $tx_height).expect("should succeed");
|
||||
)*
|
||||
)?
|
||||
|
||||
chain
|
||||
bdk_chain::local_chain::Update {
|
||||
tip: bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect())
|
||||
.tip()
|
||||
.expect("must have tip"),
|
||||
introduce_older_blocks: true,
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
@@ -107,11 +107,11 @@ fn insert_relevant_txs() {
|
||||
|
||||
fn test_list_owned_txouts() {
|
||||
// Create Local chains
|
||||
|
||||
let local_chain = (0..150)
|
||||
.map(|i| (i as u32, h!("random")))
|
||||
.collect::<BTreeMap<u32, BlockHash>>();
|
||||
let local_chain = LocalChain::from(local_chain);
|
||||
let local_chain = LocalChain::from(
|
||||
(0..150)
|
||||
.map(|i| (i as u32, h!("random")))
|
||||
.collect::<BTreeMap<u32, BlockHash>>(),
|
||||
);
|
||||
|
||||
// Initiate IndexedTxGraph
|
||||
|
||||
@@ -214,7 +214,8 @@ fn test_list_owned_txouts() {
|
||||
local_chain
|
||||
.blocks()
|
||||
.get(&height)
|
||||
.map(|&hash| BlockId { height, hash })
|
||||
.cloned()
|
||||
.map(|hash| BlockId { height, hash })
|
||||
.map(|anchor_block| ConfirmationHeightAnchor {
|
||||
anchor_block,
|
||||
confirmation_height: anchor_block.height,
|
||||
@@ -234,7 +235,7 @@ fn test_list_owned_txouts() {
|
||||
.blocks()
|
||||
.get(&height)
|
||||
.map(|&hash| BlockId { height, hash })
|
||||
.expect("block must exist");
|
||||
.unwrap_or_else(|| panic!("block must exist at {}", height));
|
||||
let txouts = graph
|
||||
.graph()
|
||||
.filter_chain_txouts(
|
||||
|
||||
@@ -1,180 +1,300 @@
|
||||
use bdk_chain::local_chain::{
|
||||
ChangeSet, InsertBlockNotMatchingError, LocalChain, UpdateNotConnectedError,
|
||||
};
|
||||
use bdk_chain::local_chain::{CannotConnectError, ChangeSet, InsertBlockError, LocalChain, Update};
|
||||
use bitcoin::BlockHash;
|
||||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn add_first_tip() {
|
||||
let chain = LocalChain::default();
|
||||
assert_eq!(
|
||||
chain.determine_changeset(&local_chain![(0, h!("A"))]),
|
||||
Ok([(0, Some(h!("A")))].into()),
|
||||
"add first tip"
|
||||
);
|
||||
#[derive(Debug)]
|
||||
struct TestLocalChain<'a> {
|
||||
name: &'static str,
|
||||
chain: LocalChain,
|
||||
update: Update,
|
||||
exp: ExpectedResult<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum ExpectedResult<'a> {
|
||||
Ok {
|
||||
changeset: &'a [(u32, Option<BlockHash>)],
|
||||
init_changeset: &'a [(u32, Option<BlockHash>)],
|
||||
},
|
||||
Err(CannotConnectError),
|
||||
}
|
||||
|
||||
impl<'a> TestLocalChain<'a> {
|
||||
fn run(mut self) {
|
||||
println!("[TestLocalChain] test: {}", self.name);
|
||||
let got_changeset = match self.chain.apply_update(self.update) {
|
||||
Ok(changeset) => changeset,
|
||||
Err(got_err) => {
|
||||
assert_eq!(
|
||||
ExpectedResult::Err(got_err),
|
||||
self.exp,
|
||||
"{}: unexpected error",
|
||||
self.name
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match self.exp {
|
||||
ExpectedResult::Ok {
|
||||
changeset,
|
||||
init_changeset,
|
||||
} => {
|
||||
assert_eq!(
|
||||
got_changeset,
|
||||
changeset.iter().cloned().collect(),
|
||||
"{}: unexpected changeset",
|
||||
self.name
|
||||
);
|
||||
assert_eq!(
|
||||
self.chain.initial_changeset(),
|
||||
init_changeset.iter().cloned().collect(),
|
||||
"{}: unexpected initial changeset",
|
||||
self.name
|
||||
);
|
||||
}
|
||||
ExpectedResult::Err(err) => panic!(
|
||||
"{}: expected error ({}), got non-error result: {:?}",
|
||||
self.name, err, got_changeset
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_second_tip() {
|
||||
let chain = local_chain![(0, h!("A"))];
|
||||
assert_eq!(
|
||||
chain.determine_changeset(&local_chain![(0, h!("A")), (1, h!("B"))]),
|
||||
Ok([(1, Some(h!("B")))].into())
|
||||
);
|
||||
fn update_local_chain() {
|
||||
[
|
||||
TestLocalChain {
|
||||
name: "add first tip",
|
||||
chain: local_chain![],
|
||||
update: chain_update![(0, h!("A"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(0, Some(h!("A")))],
|
||||
init_changeset: &[(0, Some(h!("A")))],
|
||||
},
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "add second tip",
|
||||
chain: local_chain![(0, h!("A"))],
|
||||
update: chain_update![(0, h!("A")), (1, h!("B"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(1, Some(h!("B")))],
|
||||
init_changeset: &[(0, Some(h!("A"))), (1, Some(h!("B")))],
|
||||
},
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "two disjoint chains cannot merge",
|
||||
chain: local_chain![(0, h!("A"))],
|
||||
update: chain_update![(1, h!("B"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError {
|
||||
try_include_height: 0,
|
||||
}),
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "two disjoint chains cannot merge (existing chain longer)",
|
||||
chain: local_chain![(1, h!("A"))],
|
||||
update: chain_update![(0, h!("B"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError {
|
||||
try_include_height: 1,
|
||||
}),
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "duplicate chains should merge",
|
||||
chain: local_chain![(0, h!("A"))],
|
||||
update: chain_update![(0, h!("A"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[],
|
||||
init_changeset: &[(0, Some(h!("A")))],
|
||||
},
|
||||
},
|
||||
// Introduce an older checkpoint (B)
|
||||
// | 0 | 1 | 2 | 3
|
||||
// chain | C D
|
||||
// update | B C
|
||||
TestLocalChain {
|
||||
name: "can introduce older checkpoint",
|
||||
chain: local_chain![(2, h!("C")), (3, h!("D"))],
|
||||
update: chain_update![(1, h!("B")), (2, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(1, Some(h!("B")))],
|
||||
init_changeset: &[(1, Some(h!("B"))), (2, Some(h!("C"))), (3, Some(h!("D")))],
|
||||
},
|
||||
},
|
||||
// Introduce an older checkpoint (A) that is not directly behind PoA
|
||||
// | 2 | 3 | 4
|
||||
// chain | B C
|
||||
// update | A C
|
||||
TestLocalChain {
|
||||
name: "can introduce older checkpoint 2",
|
||||
chain: local_chain![(3, h!("B")), (4, h!("C"))],
|
||||
update: chain_update![(2, h!("A")), (4, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(2, Some(h!("A")))],
|
||||
init_changeset: &[(2, Some(h!("A"))), (3, Some(h!("B"))), (4, Some(h!("C")))],
|
||||
}
|
||||
},
|
||||
// Introduce an older checkpoint (B) that is not the oldest checkpoint
|
||||
// | 1 | 2 | 3
|
||||
// chain | A C
|
||||
// update | B C
|
||||
TestLocalChain {
|
||||
name: "can introduce older checkpoint 3",
|
||||
chain: local_chain![(1, h!("A")), (3, h!("C"))],
|
||||
update: chain_update![(2, h!("B")), (3, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(2, Some(h!("B")))],
|
||||
init_changeset: &[(1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
|
||||
}
|
||||
},
|
||||
// Introduce two older checkpoints below the PoA
|
||||
// | 1 | 2 | 3
|
||||
// chain | C
|
||||
// update | A B C
|
||||
TestLocalChain {
|
||||
name: "introduce two older checkpoints below PoA",
|
||||
chain: local_chain![(3, h!("C"))],
|
||||
update: chain_update![(1, h!("A")), (2, h!("B")), (3, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(1, Some(h!("A"))), (2, Some(h!("B")))],
|
||||
init_changeset: &[(1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
|
||||
},
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "fix blockhash before agreement point",
|
||||
chain: local_chain![(0, h!("im-wrong")), (1, h!("we-agree"))],
|
||||
update: chain_update![(0, h!("fix")), (1, h!("we-agree"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(0, Some(h!("fix")))],
|
||||
init_changeset: &[(0, Some(h!("fix"))), (1, Some(h!("we-agree")))],
|
||||
},
|
||||
},
|
||||
// B and C are in both chain and update
|
||||
// | 0 | 1 | 2 | 3 | 4
|
||||
// chain | B C
|
||||
// update | A B C D
|
||||
// This should succeed with the point of agreement being C and A should be added in addition.
|
||||
TestLocalChain {
|
||||
name: "two points of agreement",
|
||||
chain: local_chain![(1, h!("B")), (2, h!("C"))],
|
||||
update: chain_update![(0, h!("A")), (1, h!("B")), (2, h!("C")), (3, h!("D"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(0, Some(h!("A"))), (3, Some(h!("D")))],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("A"))),
|
||||
(1, Some(h!("B"))),
|
||||
(2, Some(h!("C"))),
|
||||
(3, Some(h!("D"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Update and chain does not connect:
|
||||
// | 0 | 1 | 2 | 3 | 4
|
||||
// chain | B C
|
||||
// update | A B D
|
||||
// This should fail as we cannot figure out whether C & D are on the same chain
|
||||
TestLocalChain {
|
||||
name: "update and chain does not connect",
|
||||
chain: local_chain![(1, h!("B")), (2, h!("C"))],
|
||||
update: chain_update![(0, h!("A")), (1, h!("B")), (3, h!("D"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError {
|
||||
try_include_height: 2,
|
||||
}),
|
||||
},
|
||||
// Transient invalidation:
|
||||
// | 0 | 1 | 2 | 3 | 4 | 5
|
||||
// chain | A B C E
|
||||
// update | A B' C' D
|
||||
// This should succeed and invalidate B,C and E with point of agreement being A.
|
||||
TestLocalChain {
|
||||
name: "transitive invalidation applies to checkpoints higher than invalidation",
|
||||
chain: local_chain![(0, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
|
||||
update: chain_update![(0, h!("A")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[
|
||||
(2, Some(h!("B'"))),
|
||||
(3, Some(h!("C'"))),
|
||||
(4, Some(h!("D"))),
|
||||
(5, None),
|
||||
],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("A"))),
|
||||
(2, Some(h!("B'"))),
|
||||
(3, Some(h!("C'"))),
|
||||
(4, Some(h!("D"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Transient invalidation:
|
||||
// | 0 | 1 | 2 | 3 | 4
|
||||
// chain | B C E
|
||||
// update | B' C' D
|
||||
// This should succeed and invalidate B, C and E with no point of agreement
|
||||
TestLocalChain {
|
||||
name: "transitive invalidation applies to checkpoints higher than invalidation no point of agreement",
|
||||
chain: local_chain![(1, h!("B")), (2, h!("C")), (4, h!("E"))],
|
||||
update: chain_update![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[
|
||||
(1, Some(h!("B'"))),
|
||||
(2, Some(h!("C'"))),
|
||||
(3, Some(h!("D"))),
|
||||
(4, None)
|
||||
],
|
||||
init_changeset: &[
|
||||
(1, Some(h!("B'"))),
|
||||
(2, Some(h!("C'"))),
|
||||
(3, Some(h!("D"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Transient invalidation:
|
||||
// | 0 | 1 | 2 | 3 | 4
|
||||
// chain | A B C E
|
||||
// update | B' C' D
|
||||
// This should fail since although it tells us that B and C are invalid it doesn't tell us whether
|
||||
// A was invalid.
|
||||
TestLocalChain {
|
||||
name: "invalidation but no connection",
|
||||
chain: local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (4, h!("E"))],
|
||||
update: chain_update![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError { try_include_height: 0 }),
|
||||
},
|
||||
// Introduce blocks between two points of agreement
|
||||
// | 0 | 1 | 2 | 3 | 4 | 5
|
||||
// chain | A B D E
|
||||
// update | A C E F
|
||||
TestLocalChain {
|
||||
name: "introduce blocks between two points of agreement",
|
||||
chain: local_chain![(0, h!("A")), (1, h!("B")), (3, h!("D")), (4, h!("E"))],
|
||||
update: chain_update![(0, h!("A")), (2, h!("C")), (4, h!("E")), (5, h!("F"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[
|
||||
(2, Some(h!("C"))),
|
||||
(5, Some(h!("F"))),
|
||||
],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("A"))),
|
||||
(1, Some(h!("B"))),
|
||||
(2, Some(h!("C"))),
|
||||
(3, Some(h!("D"))),
|
||||
(4, Some(h!("E"))),
|
||||
(5, Some(h!("F"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(TestLocalChain::run);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_disjoint_chains_cannot_merge() {
|
||||
let chain1 = local_chain![(0, h!("A"))];
|
||||
let chain2 = local_chain![(1, h!("B"))];
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Err(UpdateNotConnectedError(0))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_chains_should_merge() {
|
||||
let chain1 = local_chain![(0, h!("A"))];
|
||||
let chain2 = local_chain![(0, h!("A"))];
|
||||
assert_eq!(chain1.determine_changeset(&chain2), Ok(Default::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_introduce_older_checkpoints() {
|
||||
let chain1 = local_chain![(2, h!("C")), (3, h!("D"))];
|
||||
let chain2 = local_chain![(1, h!("B")), (2, h!("C"))];
|
||||
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Ok([(1, Some(h!("B")))].into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fix_blockhash_before_agreement_point() {
|
||||
let chain1 = local_chain![(0, h!("im-wrong")), (1, h!("we-agree"))];
|
||||
let chain2 = local_chain![(0, h!("fix")), (1, h!("we-agree"))];
|
||||
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Ok([(0, Some(h!("fix")))].into())
|
||||
)
|
||||
}
|
||||
|
||||
/// B and C are in both chain and update
|
||||
/// ```
|
||||
/// | 0 | 1 | 2 | 3 | 4
|
||||
/// chain | B C
|
||||
/// update | A B C D
|
||||
/// ```
|
||||
/// This should succeed with the point of agreement being C and A should be added in addition.
|
||||
#[test]
|
||||
fn two_points_of_agreement() {
|
||||
let chain1 = local_chain![(1, h!("B")), (2, h!("C"))];
|
||||
let chain2 = local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (3, h!("D"))];
|
||||
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Ok([(0, Some(h!("A"))), (3, Some(h!("D")))].into()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Update and chain does not connect:
|
||||
/// ```
|
||||
/// | 0 | 1 | 2 | 3 | 4
|
||||
/// chain | B C
|
||||
/// update | A B D
|
||||
/// ```
|
||||
/// This should fail as we cannot figure out whether C & D are on the same chain
|
||||
#[test]
|
||||
fn update_and_chain_does_not_connect() {
|
||||
let chain1 = local_chain![(1, h!("B")), (2, h!("C"))];
|
||||
let chain2 = local_chain![(0, h!("A")), (1, h!("B")), (3, h!("D"))];
|
||||
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Err(UpdateNotConnectedError(2)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Transient invalidation:
|
||||
/// ```
|
||||
/// | 0 | 1 | 2 | 3 | 4 | 5
|
||||
/// chain | A B C E
|
||||
/// update | A B' C' D
|
||||
/// ```
|
||||
/// This should succeed and invalidate B,C and E with point of agreement being A.
|
||||
#[test]
|
||||
fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation() {
|
||||
let chain1 = local_chain![(0, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))];
|
||||
let chain2 = local_chain![(0, h!("A")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))];
|
||||
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Ok([
|
||||
(2, Some(h!("B'"))),
|
||||
(3, Some(h!("C'"))),
|
||||
(4, Some(h!("D"))),
|
||||
(5, None),
|
||||
]
|
||||
.into())
|
||||
);
|
||||
}
|
||||
|
||||
/// Transient invalidation:
|
||||
/// ```
|
||||
/// | 0 | 1 | 2 | 3 | 4
|
||||
/// chain | B C E
|
||||
/// update | B' C' D
|
||||
/// ```
|
||||
///
|
||||
/// This should succeed and invalidate B, C and E with no point of agreement
|
||||
#[test]
|
||||
fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation_no_point_of_agreement() {
|
||||
let chain1 = local_chain![(1, h!("B")), (2, h!("C")), (4, h!("E"))];
|
||||
let chain2 = local_chain![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))];
|
||||
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Ok([
|
||||
(1, Some(h!("B'"))),
|
||||
(2, Some(h!("C'"))),
|
||||
(3, Some(h!("D"))),
|
||||
(4, None)
|
||||
]
|
||||
.into())
|
||||
)
|
||||
}
|
||||
|
||||
/// Transient invalidation:
|
||||
/// ```
|
||||
/// | 0 | 1 | 2 | 3 | 4
|
||||
/// chain | A B C E
|
||||
/// update | B' C' D
|
||||
/// ```
|
||||
///
|
||||
/// This should fail since although it tells us that B and C are invalid it doesn't tell us whether
|
||||
/// A was invalid.
|
||||
#[test]
|
||||
fn invalidation_but_no_connection() {
|
||||
let chain1 = local_chain![(0, h!("A")), (1, h!("B")), (2, h!("C")), (4, h!("E"))];
|
||||
let chain2 = local_chain![(1, h!("B'")), (2, h!("C'")), (3, h!("D"))];
|
||||
|
||||
assert_eq!(
|
||||
chain1.determine_changeset(&chain2),
|
||||
Err(UpdateNotConnectedError(0))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_block() {
|
||||
fn local_chain_insert_block() {
|
||||
struct TestCase {
|
||||
original: LocalChain,
|
||||
insert: (u32, BlockHash),
|
||||
expected_result: Result<ChangeSet, InsertBlockNotMatchingError>,
|
||||
expected_result: Result<ChangeSet, InsertBlockError>,
|
||||
expected_final: LocalChain,
|
||||
}
|
||||
|
||||
@@ -206,7 +326,7 @@ fn insert_block() {
|
||||
TestCase {
|
||||
original: local_chain![(2, h!("K"))],
|
||||
insert: (2, h!("J")),
|
||||
expected_result: Err(InsertBlockNotMatchingError {
|
||||
expected_result: Err(InsertBlockError {
|
||||
height: 2,
|
||||
original_hash: h!("K"),
|
||||
update_hash: h!("J"),
|
||||
|
||||
@@ -4,7 +4,7 @@ use bdk_chain::{
|
||||
collections::*,
|
||||
local_chain::LocalChain,
|
||||
tx_graph::{Additions, TxGraph},
|
||||
Append, BlockId, ChainPosition, ConfirmationHeightAnchor,
|
||||
Anchor, Append, BlockId, ChainPosition, ConfirmationHeightAnchor,
|
||||
};
|
||||
use bitcoin::{
|
||||
hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid,
|
||||
@@ -697,7 +697,7 @@ fn test_chain_spends() {
|
||||
let _ = graph.insert_anchor(
|
||||
tx.txid(),
|
||||
ConfirmationHeightAnchor {
|
||||
anchor_block: tip,
|
||||
anchor_block: tip.block_id(),
|
||||
confirmation_height: *ht,
|
||||
},
|
||||
);
|
||||
@@ -705,10 +705,10 @@ fn test_chain_spends() {
|
||||
|
||||
// Assert that confirmed spends are returned correctly.
|
||||
assert_eq!(
|
||||
graph.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 0)),
|
||||
graph.get_chain_spend(&local_chain, tip.block_id(), OutPoint::new(tx_0.txid(), 0)),
|
||||
Some((
|
||||
ChainPosition::Confirmed(&ConfirmationHeightAnchor {
|
||||
anchor_block: tip,
|
||||
anchor_block: tip.block_id(),
|
||||
confirmation_height: 98
|
||||
}),
|
||||
tx_1.txid(),
|
||||
@@ -717,17 +717,17 @@ fn test_chain_spends() {
|
||||
|
||||
// Check if chain position is returned correctly.
|
||||
assert_eq!(
|
||||
graph.get_chain_position(&local_chain, tip, tx_0.txid()),
|
||||
graph.get_chain_position(&local_chain, tip.block_id(), tx_0.txid()),
|
||||
// Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))),
|
||||
Some(ChainPosition::Confirmed(&ConfirmationHeightAnchor {
|
||||
anchor_block: tip,
|
||||
anchor_block: tip.block_id(),
|
||||
confirmation_height: 95
|
||||
}))
|
||||
);
|
||||
|
||||
// Even if unconfirmed tx has a last_seen of 0, it can still be part of a chain spend.
|
||||
assert_eq!(
|
||||
graph.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1)),
|
||||
graph.get_chain_spend(&local_chain, tip.block_id(), OutPoint::new(tx_0.txid(), 1)),
|
||||
Some((ChainPosition::Unconfirmed(0), tx_2.txid())),
|
||||
);
|
||||
|
||||
@@ -737,7 +737,7 @@ fn test_chain_spends() {
|
||||
// Check chain spend returned correctly.
|
||||
assert_eq!(
|
||||
graph
|
||||
.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1))
|
||||
.get_chain_spend(&local_chain, tip.block_id(), OutPoint::new(tx_0.txid(), 1))
|
||||
.unwrap(),
|
||||
(ChainPosition::Unconfirmed(1234567), tx_2.txid())
|
||||
);
|
||||
@@ -754,7 +754,7 @@ fn test_chain_spends() {
|
||||
|
||||
// Because this tx conflicts with an already confirmed transaction, chain position should return none.
|
||||
assert!(graph
|
||||
.get_chain_position(&local_chain, tip, tx_1_conflict.txid())
|
||||
.get_chain_position(&local_chain, tip.block_id(), tx_1_conflict.txid())
|
||||
.is_none());
|
||||
|
||||
// Another conflicting tx that conflicts with tx_2.
|
||||
@@ -773,7 +773,7 @@ fn test_chain_spends() {
|
||||
// This should return a valid observation with correct last seen.
|
||||
assert_eq!(
|
||||
graph
|
||||
.get_chain_position(&local_chain, tip, tx_2_conflict.txid())
|
||||
.get_chain_position(&local_chain, tip.block_id(), tx_2_conflict.txid())
|
||||
.expect("position expected"),
|
||||
ChainPosition::Unconfirmed(1234568)
|
||||
);
|
||||
@@ -781,14 +781,14 @@ fn test_chain_spends() {
|
||||
// Chain_spend now catches the new transaction as the spend.
|
||||
assert_eq!(
|
||||
graph
|
||||
.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1))
|
||||
.get_chain_spend(&local_chain, tip.block_id(), OutPoint::new(tx_0.txid(), 1))
|
||||
.expect("expect observation"),
|
||||
(ChainPosition::Unconfirmed(1234568), tx_2_conflict.txid())
|
||||
);
|
||||
|
||||
// Chain position of the `tx_2` is now none, as it is older than `tx_2_conflict`
|
||||
assert!(graph
|
||||
.get_chain_position(&local_chain, tip, tx_2.txid())
|
||||
.get_chain_position(&local_chain, tip.block_id(), tx_2.txid())
|
||||
.is_none());
|
||||
}
|
||||
|
||||
@@ -822,3 +822,136 @@ fn test_additions_last_seen_append() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_blocks() {
|
||||
/// An anchor implementation for testing, made up of `(the_anchor_block, random_data)`.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, core::hash::Hash)]
|
||||
struct TestAnchor(BlockId);
|
||||
|
||||
impl Anchor for TestAnchor {
|
||||
fn anchor_block(&self) -> BlockId {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
struct Scenario<'a> {
|
||||
name: &'a str,
|
||||
graph: TxGraph<TestAnchor>,
|
||||
chain: LocalChain,
|
||||
exp_heights: &'a [u32],
|
||||
}
|
||||
|
||||
const fn new_anchor(height: u32, hash: BlockHash) -> TestAnchor {
|
||||
TestAnchor(BlockId { height, hash })
|
||||
}
|
||||
|
||||
fn new_scenario<'a>(
|
||||
name: &'a str,
|
||||
graph_anchors: &'a [(Txid, TestAnchor)],
|
||||
chain: &'a [(u32, BlockHash)],
|
||||
exp_heights: &'a [u32],
|
||||
) -> Scenario<'a> {
|
||||
Scenario {
|
||||
name,
|
||||
graph: {
|
||||
let mut g = TxGraph::default();
|
||||
for (txid, anchor) in graph_anchors {
|
||||
let _ = g.insert_anchor(*txid, anchor.clone());
|
||||
}
|
||||
g
|
||||
},
|
||||
chain: {
|
||||
let mut c = LocalChain::default();
|
||||
for (height, hash) in chain {
|
||||
let _ = c.insert_block(BlockId {
|
||||
height: *height,
|
||||
hash: *hash,
|
||||
});
|
||||
}
|
||||
c
|
||||
},
|
||||
exp_heights,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(scenarios: &[Scenario]) {
|
||||
for scenario in scenarios {
|
||||
let Scenario {
|
||||
name,
|
||||
graph,
|
||||
chain,
|
||||
exp_heights,
|
||||
} = scenario;
|
||||
|
||||
let heights = graph.missing_heights(chain).collect::<Vec<_>>();
|
||||
assert_eq!(&heights, exp_heights, "scenario: {}", name);
|
||||
}
|
||||
}
|
||||
|
||||
run(&[
|
||||
new_scenario(
|
||||
"2 txs with the same anchor (2:B) which is missing from chain",
|
||||
&[
|
||||
(h!("tx_1"), new_anchor(2, h!("B"))),
|
||||
(h!("tx_2"), new_anchor(2, h!("B"))),
|
||||
],
|
||||
&[(1, h!("A")), (3, h!("C"))],
|
||||
&[2],
|
||||
),
|
||||
new_scenario(
|
||||
"2 txs with different anchors at the same height, one of the anchors is missing",
|
||||
&[
|
||||
(h!("tx_1"), new_anchor(2, h!("B1"))),
|
||||
(h!("tx_2"), new_anchor(2, h!("B2"))),
|
||||
],
|
||||
&[(1, h!("A")), (2, h!("B1"))],
|
||||
&[],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors of same height which are missing from the chain",
|
||||
&[
|
||||
(h!("tx"), new_anchor(3, h!("C1"))),
|
||||
(h!("tx"), new_anchor(3, h!("C2"))),
|
||||
],
|
||||
&[(1, h!("A")), (4, h!("D"))],
|
||||
&[3],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors at the same height, chain has this height but does not match either anchor",
|
||||
&[
|
||||
(h!("tx"), new_anchor(4, h!("D1"))),
|
||||
(h!("tx"), new_anchor(4, h!("D2"))),
|
||||
],
|
||||
&[(4, h!("D3")), (5, h!("E"))],
|
||||
&[],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors at different heights, one anchor exists in chain, should return nothing",
|
||||
&[
|
||||
(h!("tx"), new_anchor(3, h!("C"))),
|
||||
(h!("tx"), new_anchor(4, h!("D"))),
|
||||
],
|
||||
&[(4, h!("D")), (5, h!("E"))],
|
||||
&[],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors at different heights, first height is already in chain with different hash, iterator should only return 2nd height",
|
||||
&[
|
||||
(h!("tx"), new_anchor(5, h!("E1"))),
|
||||
(h!("tx"), new_anchor(6, h!("F1"))),
|
||||
],
|
||||
&[(4, h!("D")), (5, h!("E")), (7, h!("G"))],
|
||||
&[6],
|
||||
),
|
||||
new_scenario(
|
||||
"tx with 2 anchors at different heights, neither height is in chain, both heights should be returned",
|
||||
&[
|
||||
(h!("tx"), new_anchor(3, h!("C"))),
|
||||
(h!("tx"), new_anchor(4, h!("D"))),
|
||||
],
|
||||
&[(1, h!("A")), (2, h!("B"))],
|
||||
&[3, 4],
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user