Merge pull request #963 from evanlinjin/chain_redesign_tweaks
Various tweaks to redesigned structures
This commit is contained in:
commit
e3c137043f
@ -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> {
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {}
|
||||||
|
@ -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 {
|
||||||
|
@ -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`.
|
||||||
|
@ -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);
|
||||||
|
@ -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,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user