Merge pull request #963 from evanlinjin/chain_redesign_tweaks

Various tweaks to redesigned structures
This commit is contained in:
志宇 2023-05-05 20:11:11 +08:00 committed by GitHub
commit e3c137043f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 301 additions and 207 deletions

View File

@ -160,12 +160,6 @@ impl Default for BlockId {
} }
} }
impl Anchor for BlockId {
fn anchor_block(&self) -> BlockId {
*self
}
}
impl From<(u32, BlockHash)> for BlockId { impl From<(u32, BlockHash)> for BlockId {
fn from((height, hash): (u32, BlockHash)) -> Self { fn from((height, hash): (u32, BlockHash)) -> Self {
Self { height, hash } Self { height, hash }
@ -187,6 +181,58 @@ impl From<(&u32, &BlockHash)> for BlockId {
} }
} }
/// An [`Anchor`] implementation that also records the exact confirmation height of the transaction.
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(crate = "serde_crate")
)]
pub struct ConfirmationHeightAnchor {
/// The anchor block.
pub anchor_block: BlockId,
/// The exact confirmation height of the transaction.
///
/// It is assumed that this value is never larger than the height of the anchor block.
pub confirmation_height: u32,
}
impl Anchor for ConfirmationHeightAnchor {
fn anchor_block(&self) -> BlockId {
self.anchor_block
}
fn confirmation_height_upper_bound(&self) -> u32 {
self.confirmation_height
}
}
/// An [`Anchor`] implementation that also records the exact confirmation time and height of the
/// transaction.
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize, serde::Serialize),
serde(crate = "serde_crate")
)]
pub struct ConfirmationTimeAnchor {
/// The anchor block.
pub anchor_block: BlockId,
pub confirmation_height: u32,
pub confirmation_time: u64,
}
impl Anchor for ConfirmationTimeAnchor {
fn anchor_block(&self) -> BlockId {
self.anchor_block
}
fn confirmation_height_upper_bound(&self) -> u32 {
self.confirmation_height
}
}
/// A `TxOut` with as much data as we can retrieve about it /// A `TxOut` with as much data as we can retrieve about it
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct FullTxOut<P> { pub struct FullTxOut<P> {

View File

@ -10,12 +10,13 @@ pub trait ChainOracle {
/// Error type. /// Error type.
type Error: core::fmt::Debug; type Error: core::fmt::Debug;
/// Determines whether `block` of [`BlockId`] exists as an ancestor of `static_block`. /// Determines whether `block` of [`BlockId`] exists as an ancestor of `chain_tip`.
/// ///
/// If `None` is returned, it means the implementation cannot determine whether `block` exists. /// If `None` is returned, it means the implementation cannot determine whether `block` exists
/// under `chain_tip`.
fn is_block_in_chain( fn is_block_in_chain(
&self, &self,
block: BlockId, block: BlockId,
static_block: BlockId, chain_tip: BlockId,
) -> Result<Option<bool>, Self::Error>; ) -> Result<Option<bool>, Self::Error>;
} }

View File

@ -12,6 +12,7 @@ use crate::{
/// A struct that combines [`TxGraph`] and an [`Indexer`] implementation. /// A struct that combines [`TxGraph`] and an [`Indexer`] implementation.
/// ///
/// This structure ensures that [`TxGraph`] and [`Indexer`] are updated atomically. /// This structure ensures that [`TxGraph`] and [`Indexer`] are updated atomically.
#[derive(Debug)]
pub struct IndexedTxGraph<A, I> { pub struct IndexedTxGraph<A, I> {
/// Transaction index. /// Transaction index.
pub index: I, pub index: I,
@ -27,12 +28,14 @@ impl<A, I: Default> Default for IndexedTxGraph<A, I> {
} }
} }
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I> { impl<A, I> IndexedTxGraph<A, I> {
/// Get a reference of the internal transaction graph. /// Get a reference of the internal transaction graph.
pub fn graph(&self) -> &TxGraph<A> { pub fn graph(&self) -> &TxGraph<A> {
&self.graph &self.graph
} }
}
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I> {
/// Applies the [`IndexedAdditions`] to the [`IndexedTxGraph`]. /// Applies the [`IndexedAdditions`] to the [`IndexedTxGraph`].
pub fn apply_additions(&mut self, additions: IndexedAdditions<A, I::Additions>) { pub fn apply_additions(&mut self, additions: IndexedAdditions<A, I::Additions>) {
let IndexedAdditions { let IndexedAdditions {
@ -217,7 +220,7 @@ impl<A: Anchor, I: OwnedIndexer> IndexedTxGraph<A, I> {
C: ChainOracle, C: ChainOracle,
F: FnMut(&Script) -> bool, F: FnMut(&Script) -> bool,
{ {
let tip_height = chain_tip.anchor_block().height; let tip_height = chain_tip.height;
let mut immature = 0; let mut immature = 0;
let mut trusted_pending = 0; let mut trusted_pending = 0;

View File

@ -89,7 +89,7 @@ impl<K> Deref for KeychainTxOutIndex<K> {
} }
} }
impl<K: Clone + Ord + Debug + 'static> Indexer for KeychainTxOutIndex<K> { impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
type Additions = DerivationAdditions<K>; type Additions = DerivationAdditions<K>;
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions {
@ -109,9 +109,9 @@ impl<K: Clone + Ord + Debug + 'static> Indexer for KeychainTxOutIndex<K> {
} }
} }
impl<K: Clone + Ord + Debug + 'static> OwnedIndexer for KeychainTxOutIndex<K> { impl<K: Clone + Ord + Debug> OwnedIndexer for KeychainTxOutIndex<K> {
fn is_spk_owned(&self, spk: &Script) -> bool { fn is_spk_owned(&self, spk: &Script) -> bool {
self.inner().is_spk_owned(spk) self.index_of_spk(spk).is_some()
} }
} }

View File

@ -6,13 +6,6 @@ use bitcoin::BlockHash;
use crate::{BlockId, ChainOracle}; use crate::{BlockId, ChainOracle};
/// This is a local implementation of [`ChainOracle`]. /// This is a local implementation of [`ChainOracle`].
///
/// TODO: We need a cache/snapshot thing for chain oracle.
/// * Minimize calls to remotes.
/// * Can we cache it forever? Should we drop stuff?
/// * Assume anything deeper than (i.e. 10) blocks won't be reorged.
/// * Is this a cache on txs or block? or both?
/// TODO: Parents of children are confirmed if children are confirmed.
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct LocalChain { pub struct LocalChain {
blocks: BTreeMap<u32, BlockHash>, blocks: BTreeMap<u32, BlockHash>,
@ -71,6 +64,11 @@ impl LocalChain {
} }
} }
/// Get a reference to a map of block height to hash.
pub fn blocks(&self) -> &BTreeMap<u32, BlockHash> {
&self.blocks
}
pub fn tip(&self) -> Option<BlockId> { pub fn tip(&self) -> Option<BlockId> {
self.blocks self.blocks
.iter() .iter()
@ -78,13 +76,6 @@ impl LocalChain {
.map(|(&height, &hash)| BlockId { height, hash }) .map(|(&height, &hash)| BlockId { height, hash })
} }
/// Get a block at the given height.
pub fn get_block(&self, height: u32) -> Option<BlockId> {
self.blocks
.get(&height)
.map(|&hash| BlockId { height, hash })
}
/// This is like the sparsechain's logic, expect we must guarantee that all invalidated heights /// This is like the sparsechain's logic, expect we must guarantee that all invalidated heights
/// are to be re-filled. /// are to be re-filled.
pub fn determine_changeset(&self, update: &Self) -> Result<ChangeSet, UpdateNotConnectedError> { pub fn determine_changeset(&self, update: &Self) -> Result<ChangeSet, UpdateNotConnectedError> {
@ -173,6 +164,31 @@ impl LocalChain {
pub fn heights(&self) -> BTreeSet<u32> { pub fn heights(&self) -> BTreeSet<u32> {
self.blocks.keys().cloned().collect() self.blocks.keys().cloned().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) {
if original_hash != block_id.hash {
return Err(InsertBlockNotMatchingError {
height: block_id.height,
original_hash,
update_hash: block_id.hash,
});
}
}
Ok(self.apply_update(update).expect("should always connect"))
}
} }
/// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`]. /// This is the return value of [`determine_changeset`] and represents changes to [`LocalChain`].
@ -201,3 +217,24 @@ impl core::fmt::Display for UpdateNotConnectedError {
#[cfg(feature = "std")] #[cfg(feature = "std")]
impl std::error::Error for UpdateNotConnectedError {} 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 height: u32,
pub original_hash: BlockHash,
pub update_hash: BlockHash,
}
impl core::fmt::Display for InsertBlockNotMatchingError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"failed to insert block at height {} as blockhashes conflict: original={}, update={}",
self.height, self.original_hash, self.update_hash
)
}
}
#[cfg(feature = "std")]
impl std::error::Error for InsertBlockNotMatchingError {}

View File

@ -53,7 +53,7 @@ impl<I> Default for SpkTxOutIndex<I> {
} }
} }
impl<I: Clone + Ord + 'static> Indexer for SpkTxOutIndex<I> { impl<I: Clone + Ord> Indexer for SpkTxOutIndex<I> {
type Additions = (); type Additions = ();
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions { fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::Additions {

View File

@ -349,6 +349,11 @@ impl<A> TxGraph<A> {
.filter(move |(_, conflicting_txid)| *conflicting_txid != txid) .filter(move |(_, conflicting_txid)| *conflicting_txid != txid)
} }
/// Get all transaction anchors known by [`TxGraph`].
pub fn all_anchors(&self) -> &BTreeSet<(A, Txid)> {
&self.anchors
}
/// Whether the graph has any transactions or outputs in it. /// Whether the graph has any transactions or outputs in it.
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.txs.is_empty() self.txs.is_empty()
@ -592,21 +597,6 @@ impl<A: Clone + Ord> TxGraph<A> {
} }
impl<A: Anchor> TxGraph<A> { impl<A: Anchor> TxGraph<A> {
/// Get all heights that are relevant to the graph.
pub fn relevant_heights(&self) -> impl Iterator<Item = u32> + '_ {
let mut last_height = Option::<u32>::None;
self.anchors
.iter()
.map(|(a, _)| a.anchor_block().height)
.filter(move |&height| {
let is_unique = Some(height) != last_height;
if is_unique {
last_height = Some(height);
}
is_unique
})
}
/// Get the position of the transaction in `chain` with tip `chain_tip`. /// 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 /// If the given transaction of `txid` does not exist in the chain of `chain_tip`, `None` is
@ -624,11 +614,9 @@ impl<A: Anchor> TxGraph<A> {
chain_tip: BlockId, chain_tip: BlockId,
txid: Txid, txid: Txid,
) -> Result<Option<ObservedAs<&A>>, C::Error> { ) -> Result<Option<ObservedAs<&A>>, C::Error> {
let (tx_node, anchors, &last_seen) = match self.txs.get(&txid) { let (tx_node, anchors, last_seen) = match self.txs.get(&txid) {
Some((tx, anchors, last_seen)) if !(anchors.is_empty() && *last_seen == 0) => { Some(v) => v,
(tx, anchors, last_seen) None => return Ok(None),
}
_ => return Ok(None),
}; };
for anchor in anchors { for anchor in anchors {
@ -657,12 +645,12 @@ impl<A: Anchor> TxGraph<A> {
return Ok(None); return Ok(None);
} }
} }
if conflicting_tx.last_seen_unconfirmed > last_seen { if conflicting_tx.last_seen_unconfirmed > *last_seen {
return Ok(None); return Ok(None);
} }
} }
Ok(Some(ObservedAs::Unconfirmed(last_seen))) Ok(Some(ObservedAs::Unconfirmed(*last_seen)))
} }
/// Get the position of the transaction in `chain` with tip `chain_tip`. /// Get the position of the transaction in `chain` with tip `chain_tip`.

View File

@ -8,7 +8,7 @@ use bdk_chain::{
keychain::{Balance, DerivationAdditions, KeychainTxOutIndex}, keychain::{Balance, DerivationAdditions, KeychainTxOutIndex},
local_chain::LocalChain, local_chain::LocalChain,
tx_graph::Additions, tx_graph::Additions,
BlockId, ObservedAs, BlockId, ConfirmationHeightAnchor, ObservedAs,
}; };
use bitcoin::{secp256k1::Secp256k1, BlockHash, OutPoint, Script, Transaction, TxIn, TxOut}; use bitcoin::{secp256k1::Secp256k1, BlockHash, OutPoint, Script, Transaction, TxIn, TxOut};
use miniscript::Descriptor; use miniscript::Descriptor;
@ -28,7 +28,7 @@ fn insert_relevant_txs() {
let spk_0 = descriptor.at_derivation_index(0).script_pubkey(); let spk_0 = descriptor.at_derivation_index(0).script_pubkey();
let spk_1 = descriptor.at_derivation_index(9).script_pubkey(); let spk_1 = descriptor.at_derivation_index(9).script_pubkey();
let mut graph = IndexedTxGraph::<BlockId, KeychainTxOutIndex<()>>::default(); let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::default();
graph.index.add_keychain((), descriptor); graph.index.add_keychain((), descriptor);
graph.index.set_lookahead(&(), 10); graph.index.set_lookahead(&(), 10);
@ -118,7 +118,8 @@ fn test_list_owned_txouts() {
let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap(); let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)").unwrap(); let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)").unwrap();
let mut graph = IndexedTxGraph::<BlockId, KeychainTxOutIndex<String>>::default(); let mut graph =
IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>::default();
graph.index.add_keychain("keychain_1".into(), desc_1); graph.index.add_keychain("keychain_1".into(), desc_1);
graph.index.add_keychain("keychain_2".into(), desc_2); graph.index.add_keychain("keychain_2".into(), desc_2);
@ -206,30 +207,45 @@ fn test_list_owned_txouts() {
// For unconfirmed txs we pass in `None`. // For unconfirmed txs we pass in `None`.
let _ = graph.insert_relevant_txs( let _ = graph.insert_relevant_txs(
[&tx1, &tx2, &tx3, &tx6] [&tx1, &tx2, &tx3, &tx6].iter().enumerate().map(|(i, tx)| {
.iter() let height = i as u32;
.enumerate() (
.map(|(i, tx)| (*tx, [local_chain.get_block(i as u32).unwrap()])), *tx,
local_chain
.blocks()
.get(&height)
.map(|&hash| BlockId { height, hash })
.map(|anchor_block| ConfirmationHeightAnchor {
anchor_block,
confirmation_height: anchor_block.height,
}),
)
}),
None, None,
); );
let _ = graph.insert_relevant_txs([&tx4, &tx5].iter().map(|tx| (*tx, None)), Some(100)); let _ = graph.insert_relevant_txs([&tx4, &tx5].iter().map(|tx| (*tx, None)), Some(100));
// A helper lambda to extract and filter data from the graph. // A helper lambda to extract and filter data from the graph.
let fetch = |ht: u32, graph: &IndexedTxGraph<BlockId, KeychainTxOutIndex<String>>| { let fetch =
|height: u32,
graph: &IndexedTxGraph<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>| {
let chain_tip = local_chain
.blocks()
.get(&height)
.map(|&hash| BlockId { height, hash })
.expect("block must exist");
let txouts = graph let txouts = graph
.list_owned_txouts(&local_chain, local_chain.get_block(ht).unwrap()) .list_owned_txouts(&local_chain, chain_tip)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let utxos = graph let utxos = graph
.list_owned_unspents(&local_chain, local_chain.get_block(ht).unwrap()) .list_owned_unspents(&local_chain, chain_tip)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let balance = graph.balance( let balance = graph.balance(&local_chain, chain_tip, |spk: &Script| {
&local_chain, trusted_spks.contains(spk)
local_chain.get_block(ht).unwrap(), });
|spk: &Script| trusted_spks.contains(spk),
);
assert_eq!(txouts.len(), 5); assert_eq!(txouts.len(), 5);
assert_eq!(utxos.len(), 4); assert_eq!(utxos.len(), 4);

View File

@ -1,4 +1,7 @@
use bdk_chain::local_chain::{LocalChain, UpdateNotConnectedError}; use bdk_chain::local_chain::{
ChangeSet, InsertBlockNotMatchingError, LocalChain, UpdateNotConnectedError,
};
use bitcoin::BlockHash;
#[macro_use] #[macro_use]
mod common; mod common;
@ -165,3 +168,61 @@ fn invalidation_but_no_connection() {
Err(UpdateNotConnectedError(0)) Err(UpdateNotConnectedError(0))
) )
} }
#[test]
fn insert_block() {
struct TestCase {
original: LocalChain,
insert: (u32, BlockHash),
expected_result: Result<ChangeSet, InsertBlockNotMatchingError>,
expected_final: LocalChain,
}
let test_cases = [
TestCase {
original: local_chain![],
insert: (5, h!("block5")),
expected_result: Ok([(5, Some(h!("block5")))].into()),
expected_final: local_chain![(5, h!("block5"))],
},
TestCase {
original: local_chain![(3, h!("A"))],
insert: (4, h!("B")),
expected_result: Ok([(4, Some(h!("B")))].into()),
expected_final: local_chain![(3, h!("A")), (4, h!("B"))],
},
TestCase {
original: local_chain![(4, h!("B"))],
insert: (3, h!("A")),
expected_result: Ok([(3, Some(h!("A")))].into()),
expected_final: local_chain![(3, h!("A")), (4, h!("B"))],
},
TestCase {
original: local_chain![(2, h!("K"))],
insert: (2, h!("K")),
expected_result: Ok([].into()),
expected_final: local_chain![(2, h!("K"))],
},
TestCase {
original: local_chain![(2, h!("K"))],
insert: (2, h!("J")),
expected_result: Err(InsertBlockNotMatchingError {
height: 2,
original_hash: h!("K"),
update_hash: h!("J"),
}),
expected_final: local_chain![(2, h!("K"))],
},
];
for (i, t) in test_cases.into_iter().enumerate() {
let mut chain = t.original;
assert_eq!(
chain.insert_block(t.insert.into()),
t.expected_result,
"[{}] unexpected result when inserting block",
i,
);
assert_eq!(chain, t.expected_final, "[{}] unexpected final chain", i,);
}
}

View File

@ -4,7 +4,7 @@ use bdk_chain::{
collections::*, collections::*,
local_chain::LocalChain, local_chain::LocalChain,
tx_graph::{Additions, TxGraph}, tx_graph::{Additions, TxGraph},
Append, BlockId, ObservedAs, Append, BlockId, ConfirmationHeightAnchor, ObservedAs,
}; };
use bitcoin::{ use bitcoin::{
hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid, hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid,
@ -684,7 +684,7 @@ fn test_chain_spends() {
..common::new_tx(0) ..common::new_tx(0)
}; };
let mut graph = TxGraph::<BlockId>::default(); let mut graph = TxGraph::<ConfirmationHeightAnchor>::default();
let _ = graph.insert_tx(tx_0.clone()); let _ = graph.insert_tx(tx_0.clone());
let _ = graph.insert_tx(tx_1.clone()); let _ = graph.insert_tx(tx_1.clone());
@ -694,33 +694,42 @@ fn test_chain_spends() {
.iter() .iter()
.zip([&tx_0, &tx_1].into_iter()) .zip([&tx_0, &tx_1].into_iter())
.for_each(|(ht, tx)| { .for_each(|(ht, tx)| {
let block_id = local_chain.get_block(*ht).expect("block expected"); let _ = graph.insert_anchor(
let _ = graph.insert_anchor(tx.txid(), block_id); tx.txid(),
ConfirmationHeightAnchor {
anchor_block: tip,
confirmation_height: *ht,
},
);
}); });
// Assert that confirmed spends are returned correctly. // Assert that confirmed spends are returned correctly.
assert_eq!( assert_eq!(
graph graph.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 0)),
.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 0)) Some((
.unwrap(), ObservedAs::Confirmed(&ConfirmationHeightAnchor {
( anchor_block: tip,
ObservedAs::Confirmed(&local_chain.get_block(98).expect("block expected")), confirmation_height: 98
tx_1.txid() }),
) tx_1.txid(),
)),
); );
// Check if chain position is returned correctly. // Check if chain position is returned correctly.
assert_eq!( assert_eq!(
graph graph.get_chain_position(&local_chain, tip, tx_0.txid()),
.get_chain_position(&local_chain, tip, tx_0.txid()) // Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))),
.expect("position expected"), Some(ObservedAs::Confirmed(&ConfirmationHeightAnchor {
ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected")) anchor_block: tip,
confirmation_height: 95
}))
); );
// As long the unconfirmed tx isn't marked as seen, chain_spend will return None. // Even if unconfirmed tx has a last_seen of 0, it can still be part of a chain spend.
assert!(graph assert_eq!(
.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1)) graph.get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1)),
.is_none()); Some((ObservedAs::Unconfirmed(0), tx_2.txid())),
);
// Mark the unconfirmed as seen and check correct ObservedAs status is returned. // Mark the unconfirmed as seen and check correct ObservedAs status is returned.
let _ = graph.insert_seen_at(tx_2.txid(), 1234567); let _ = graph.insert_seen_at(tx_2.txid(), 1234567);
@ -783,73 +792,6 @@ fn test_chain_spends() {
.is_none()); .is_none());
} }
#[test]
fn test_relevant_heights() {
let mut graph = TxGraph::<BlockId>::default();
let tx1 = common::new_tx(1);
let tx2 = common::new_tx(2);
let _ = graph.insert_tx(tx1.clone());
assert_eq!(
graph.relevant_heights().collect::<Vec<_>>(),
vec![],
"no anchors in graph"
);
let _ = graph.insert_anchor(
tx1.txid(),
BlockId {
height: 3,
hash: h!("3a"),
},
);
assert_eq!(
graph.relevant_heights().collect::<Vec<_>>(),
vec![3],
"one anchor at height 3"
);
let _ = graph.insert_anchor(
tx1.txid(),
BlockId {
height: 3,
hash: h!("3b"),
},
);
assert_eq!(
graph.relevant_heights().collect::<Vec<_>>(),
vec![3],
"introducing duplicate anchor at height 3, must not iterate over duplicate heights"
);
let _ = graph.insert_anchor(
tx1.txid(),
BlockId {
height: 4,
hash: h!("4a"),
},
);
assert_eq!(
graph.relevant_heights().collect::<Vec<_>>(),
vec![3, 4],
"anchors in height 3 and now 4"
);
let _ = graph.insert_anchor(
tx2.txid(),
BlockId {
height: 5,
hash: h!("5a"),
},
);
assert_eq!(
graph.relevant_heights().collect::<Vec<_>>(),
vec![3, 4, 5],
"anchor for non-existant tx is inserted at height 5, must still be in relevant heights",
);
}
/// Ensure that `last_seen` values only increase during [`Append::append`]. /// Ensure that `last_seen` values only increase during [`Append::append`].
#[test] #[test]
fn test_additions_last_seen_append() { fn test_additions_last_seen_append() {