Merge bitcoindevkit/bdk#1064: Better tests for transaction conflict handling
6d601a7e885bfc627594b794b1a460be47799eea test(chain): Add test for conflicting transactions (Daniela Brozzoni) 48ca95b5412fd3719b749d33c85572941817e967 test(chain): Add test for walk_ancestors (Daniela Brozzoni) 59a2403e2802d27c51f2d30b5f8e1ed9f813dfcc test(chain): Introduce TxTemplate (Daniela Brozzoni) 6e511473a5e5730d5cad237c651c83dc4c5d8756 test(chain): add block_id! utility macro (Daniela Brozzoni) 62de55f12d910ac35180cbb3f0e4c4b353c44d11 fix(chain): Consider conflicting ancestors in... ...try_get_chain_pos (Daniela Brozzoni) a3e8480ad9d41190da8de732dc8d00d636a0c911 doc(chain): Clarify direct_conflicts_of_tx's docs (Daniela Brozzoni) 4742d88ea322e43120fa96f6421a149294d26e3b feat(chain): Introduce TxAncestors, walk_ancestors (Daniela Brozzoni) 2f26eca607dc5de83a9bb12c33fa5336026ab3dd fix(chain): TxDescendants performs a BFS (Daniela Brozzoni) 486e0e143741a8c1312e7cb5258b3d43256dc9ec doc(chain): Fix typos (Daniela Brozzoni) Pull request description: <!-- You can erase any parts of this template not applicable to your Pull Request. --> ### Description Fixes #1063. This PR introduces a new `TxTemplate` struct to test different transaction conflict scenarios in `TxGraph`. The following transaction conflict scenarios are tested: - 2 unconfirmed txs with different last_seens conflict. The most recent tx should be the only tx that appears in the list methods. - 3 unconfirmed txs with different last_seens conflict. The most recent tx should be the only tx that appears in the list methods. - An unconfirmed tx U conflicts with a tx anchored in orphaned block O. O has higher last_seen. O should be the only tx that appears in the list methods. - An unconfirmed tx U conflicts with a tx anchored in orphaned block O. U has higher last_seen. U should be the only tx that appears in the list methods. - Multiple unconfirmed txs conflict with a confirmed tx. None of the unconfirmed txs should appear in the list methods. - B and B' conflict. C spends B. B' is anchored in best chain. B and C should not appear in the list methods. - B and B' conflict. C spends B. B is anchored in best chain. B' should not appear in the list methods. - B and B' conflict. C spends both B and B'. C is impossible. - B and B' conflict. C spends both B and B'. C is impossible. B' is confirmed. - B and B' conflict. C spends both B and B'. C is impossible. D spends C. These tests revealed that `TxGraph::walk_conflicts` was not checking ancestors of the root tx for conflicts. `TxGraph::walk_conflicts` has been refactored to check for conflicting ancestor transactions by using a new `TxAncestors` iterator in `TxGraph`. ### Changelog notice - Introduced `tx_template` module - Introduced `TxGraph::TxAncestors` iterator - Refactored `TxGraph::walk_conflicts` to use `TxGraph::TxAncestors` - Added `walk_ancestors` to `TxGraph` ### 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: evanlinjin: ACK 6d601a7e885bfc627594b794b1a460be47799eea Tree-SHA512: ea151392874c4312233e4e10299579f4eee4a7100ae344b4d7f19994284b49c1e43f37338bed931d16e77326021166ea0b94d6de3ccf50a8fabb25139a8e69b4
This commit is contained in:
commit
4ee11aae12
@ -54,6 +54,7 @@ use crate::{
|
||||
collections::*, keychain::Balance, local_chain::LocalChain, Anchor, Append, BlockId,
|
||||
ChainOracle, ChainPosition, FullTxOut,
|
||||
};
|
||||
use alloc::collections::vec_deque::VecDeque;
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
|
||||
use core::{
|
||||
@ -319,15 +320,39 @@ impl<A> TxGraph<A> {
|
||||
.map(|(outpoint, spends)| (outpoint.vout, spends))
|
||||
}
|
||||
|
||||
/// Creates an iterator that filters and maps ancestor transactions.
|
||||
///
|
||||
/// The iterator starts with the ancestors of the supplied `tx` (ancestor transactions of `tx`
|
||||
/// are transactions spent by `tx`). The supplied transaction is excluded from the iterator.
|
||||
///
|
||||
/// The supplied closure takes in two inputs `(depth, ancestor_tx)`:
|
||||
///
|
||||
/// * `depth` is the distance between the starting `Transaction` and the `ancestor_tx`. I.e., if
|
||||
/// the `Transaction` is spending an output of the `ancestor_tx` then `depth` will be 1.
|
||||
/// * `ancestor_tx` is the `Transaction`'s ancestor which we are considering to walk.
|
||||
///
|
||||
/// The supplied closure returns an `Option<T>`, allowing the caller to map each `Transaction`
|
||||
/// it visits and decide whether to visit ancestors.
|
||||
pub fn walk_ancestors<'g, F, O>(
|
||||
&'g self,
|
||||
tx: &'g Transaction,
|
||||
walk_map: F,
|
||||
) -> TxAncestors<'g, A, F>
|
||||
where
|
||||
F: FnMut(usize, &'g Transaction) -> Option<O> + 'g,
|
||||
{
|
||||
TxAncestors::new_exclude_root(self, tx, walk_map)
|
||||
}
|
||||
|
||||
/// Creates an iterator that filters and maps descendants from the starting `txid`.
|
||||
///
|
||||
/// The supplied closure takes in two inputs `(depth, descendant_txid)`:
|
||||
///
|
||||
/// * `depth` is the distance between the starting `txid` and the `descendant_txid`. I.e., if the
|
||||
/// descendant is spending an output of the starting `txid`; the `depth` will be 1.
|
||||
/// descendant is spending an output of the starting `txid` then `depth` will be 1.
|
||||
/// * `descendant_txid` is the descendant's txid which we are considering to walk.
|
||||
///
|
||||
/// The supplied closure returns an `Option<T>`, allowing the caller to map each node it vists
|
||||
/// The supplied closure returns an `Option<T>`, allowing the caller to map each node it visits
|
||||
/// and decide whether to visit descendants.
|
||||
pub fn walk_descendants<'g, F, O>(&'g self, txid: Txid, walk_map: F) -> TxDescendants<A, F>
|
||||
where
|
||||
@ -356,8 +381,9 @@ impl<A> TxGraph<A> {
|
||||
/// transaction's inputs (spends). The conflicting txids are returned with the given
|
||||
/// transaction's vin (in which it conflicts).
|
||||
///
|
||||
/// Note that this only returns directly conflicting txids and does not include descendants of
|
||||
/// those txids (which are technically also conflicting).
|
||||
/// Note that this only returns directly conflicting txids and won't include:
|
||||
/// - descendants of conflicting transactions (which are technically also conflicting)
|
||||
/// - transactions conflicting with the given transaction's ancestors
|
||||
pub fn direct_conflicts_of_tx<'g>(
|
||||
&'g self,
|
||||
tx: &'g Transaction,
|
||||
@ -671,8 +697,9 @@ impl<A: Anchor> TxGraph<A> {
|
||||
}
|
||||
}
|
||||
|
||||
// The tx is not anchored to a block which is in the best chain, let's check whether we can
|
||||
// ignore it by checking conflicts!
|
||||
// The tx is not anchored to a block which is in the best chain, which means that it
|
||||
// might be in mempool, or it might have been dropped already.
|
||||
// Let's check conflicts to find out!
|
||||
let tx = match tx_node {
|
||||
TxNodeInternal::Whole(tx) => tx,
|
||||
TxNodeInternal::Partial(_) => {
|
||||
@ -681,18 +708,71 @@ impl<A: Anchor> TxGraph<A> {
|
||||
}
|
||||
};
|
||||
|
||||
// If a conflicting tx is in the best chain, or has `last_seen` higher than this tx, then
|
||||
// this tx cannot exist in the best chain
|
||||
for conflicting_tx in self.walk_conflicts(tx, |_, txid| self.get_tx_node(txid)) {
|
||||
for block in conflicting_tx.anchors.iter().map(A::anchor_block) {
|
||||
if chain.is_block_in_chain(block, chain_tip)? == Some(true) {
|
||||
// conflicting tx is in best chain, so the current tx cannot be in best chain!
|
||||
// We want to retrieve all the transactions that conflict with us, plus all the
|
||||
// transactions that conflict with our unconfirmed ancestors, since they conflict with us
|
||||
// as well.
|
||||
// We only traverse unconfirmed ancestors since conflicts of confirmed transactions
|
||||
// cannot be in the best chain.
|
||||
|
||||
// First of all, we retrieve all our ancestors. Since we're using `new_include_root`, the
|
||||
// resulting array will also include `tx`
|
||||
let unconfirmed_ancestor_txs =
|
||||
TxAncestors::new_include_root(self, tx, |_, ancestor_tx: &Transaction| {
|
||||
let tx_node = self.get_tx_node(ancestor_tx.txid())?;
|
||||
// We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
|
||||
// the best chain)
|
||||
for block in tx_node.anchors {
|
||||
match chain.is_block_in_chain(block.anchor_block(), chain_tip) {
|
||||
Ok(Some(true)) => return None,
|
||||
Err(e) => return Some(Err(e)),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
Some(Ok(tx_node))
|
||||
})
|
||||
.collect::<Result<Vec<_>, C::Error>>()?;
|
||||
|
||||
// We determine our tx's last seen, which is the max between our last seen,
|
||||
// and our unconf descendants' last seen.
|
||||
let unconfirmed_descendants_txs =
|
||||
TxDescendants::new_include_root(self, tx.txid(), |_, descendant_txid: Txid| {
|
||||
let tx_node = self.get_tx_node(descendant_txid)?;
|
||||
// We're filtering the ancestors to keep only the unconfirmed ones (= no anchors in
|
||||
// the best chain)
|
||||
for block in tx_node.anchors {
|
||||
match chain.is_block_in_chain(block.anchor_block(), chain_tip) {
|
||||
Ok(Some(true)) => return None,
|
||||
Err(e) => return Some(Err(e)),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
Some(Ok(tx_node))
|
||||
})
|
||||
.collect::<Result<Vec<_>, C::Error>>()?;
|
||||
|
||||
let tx_last_seen = unconfirmed_descendants_txs
|
||||
.iter()
|
||||
.max_by_key(|tx| tx.last_seen_unconfirmed)
|
||||
.map(|tx| tx.last_seen_unconfirmed)
|
||||
.expect("descendants always includes at least one transaction (the root tx");
|
||||
|
||||
// Now we traverse our ancestors and consider all their conflicts
|
||||
for tx_node in unconfirmed_ancestor_txs {
|
||||
// We retrieve all the transactions conflicting with this specific ancestor
|
||||
let conflicting_txs = self.walk_conflicts(tx_node.tx, |_, txid| self.get_tx_node(txid));
|
||||
|
||||
// If a conflicting tx is in the best chain, or has `last_seen` higher than this ancestor, then
|
||||
// this tx cannot exist in the best chain
|
||||
for conflicting_tx in conflicting_txs {
|
||||
for block in conflicting_tx.anchors {
|
||||
if chain.is_block_in_chain(block.anchor_block(), chain_tip)? == Some(true) {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
if conflicting_tx.last_seen_unconfirmed > tx_last_seen {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
if conflicting_tx.last_seen_unconfirmed > *last_seen {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(ChainPosition::Unconfirmed(*last_seen)))
|
||||
@ -1137,6 +1217,126 @@ impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator that traverses ancestors of a given root transaction.
|
||||
///
|
||||
/// The iterator excludes partial transactions.
|
||||
///
|
||||
/// This `struct` is created by the [`walk_ancestors`] method of [`TxGraph`].
|
||||
///
|
||||
/// [`walk_ancestors`]: TxGraph::walk_ancestors
|
||||
pub struct TxAncestors<'g, A, F> {
|
||||
graph: &'g TxGraph<A>,
|
||||
visited: HashSet<Txid>,
|
||||
queue: VecDeque<(usize, &'g Transaction)>,
|
||||
filter_map: F,
|
||||
}
|
||||
|
||||
impl<'g, A, F> TxAncestors<'g, A, F> {
|
||||
/// Creates a `TxAncestors` that includes the starting `Transaction` when iterating.
|
||||
pub(crate) fn new_include_root(
|
||||
graph: &'g TxGraph<A>,
|
||||
tx: &'g Transaction,
|
||||
filter_map: F,
|
||||
) -> Self {
|
||||
Self {
|
||||
graph,
|
||||
visited: Default::default(),
|
||||
queue: [(0, tx)].into(),
|
||||
filter_map,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `TxAncestors` that excludes the starting `Transaction` when iterating.
|
||||
pub(crate) fn new_exclude_root(
|
||||
graph: &'g TxGraph<A>,
|
||||
tx: &'g Transaction,
|
||||
filter_map: F,
|
||||
) -> Self {
|
||||
let mut ancestors = Self {
|
||||
graph,
|
||||
visited: Default::default(),
|
||||
queue: Default::default(),
|
||||
filter_map,
|
||||
};
|
||||
ancestors.populate_queue(1, tx);
|
||||
ancestors
|
||||
}
|
||||
|
||||
/// Creates a `TxAncestors` from multiple starting `Transaction`s that includes the starting
|
||||
/// `Transaction`s when iterating.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn from_multiple_include_root<I>(
|
||||
graph: &'g TxGraph<A>,
|
||||
txs: I,
|
||||
filter_map: F,
|
||||
) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = &'g Transaction>,
|
||||
{
|
||||
Self {
|
||||
graph,
|
||||
visited: Default::default(),
|
||||
queue: txs.into_iter().map(|tx| (0, tx)).collect(),
|
||||
filter_map,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `TxAncestors` from multiple starting `Transaction`s that excludes the starting
|
||||
/// `Transaction`s when iterating.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn from_multiple_exclude_root<I>(
|
||||
graph: &'g TxGraph<A>,
|
||||
txs: I,
|
||||
filter_map: F,
|
||||
) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = &'g Transaction>,
|
||||
{
|
||||
let mut ancestors = Self {
|
||||
graph,
|
||||
visited: Default::default(),
|
||||
queue: Default::default(),
|
||||
filter_map,
|
||||
};
|
||||
for tx in txs {
|
||||
ancestors.populate_queue(1, tx);
|
||||
}
|
||||
ancestors
|
||||
}
|
||||
|
||||
fn populate_queue(&mut self, depth: usize, tx: &'g Transaction) {
|
||||
let ancestors = tx
|
||||
.input
|
||||
.iter()
|
||||
.map(|txin| txin.previous_output.txid)
|
||||
.filter(|&prev_txid| self.visited.insert(prev_txid))
|
||||
.filter_map(|prev_txid| self.graph.get_tx(prev_txid))
|
||||
.map(|tx| (depth, tx));
|
||||
self.queue.extend(ancestors);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'g, A, F, O> Iterator for TxAncestors<'g, A, F>
|
||||
where
|
||||
F: FnMut(usize, &'g Transaction) -> Option<O>,
|
||||
{
|
||||
type Item = O;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
// we have exhausted all paths when queue is empty
|
||||
let (ancestor_depth, tx) = self.queue.pop_front()?;
|
||||
// ignore paths when user filters them out
|
||||
let item = match (self.filter_map)(ancestor_depth, tx) {
|
||||
Some(item) => item,
|
||||
None => continue,
|
||||
};
|
||||
self.populate_queue(ancestor_depth + 1, tx);
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator that traverses transaction descendants.
|
||||
///
|
||||
/// This `struct` is created by the [`walk_descendants`] method of [`TxGraph`].
|
||||
@ -1145,7 +1345,7 @@ impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
|
||||
pub struct TxDescendants<'g, A, F> {
|
||||
graph: &'g TxGraph<A>,
|
||||
visited: HashSet<Txid>,
|
||||
stack: Vec<(usize, Txid)>,
|
||||
queue: VecDeque<(usize, Txid)>,
|
||||
filter_map: F,
|
||||
}
|
||||
|
||||
@ -1156,7 +1356,7 @@ impl<'g, A, F> TxDescendants<'g, A, F> {
|
||||
Self {
|
||||
graph,
|
||||
visited: Default::default(),
|
||||
stack: [(0, txid)].into(),
|
||||
queue: [(0, txid)].into(),
|
||||
filter_map,
|
||||
}
|
||||
}
|
||||
@ -1166,14 +1366,14 @@ impl<'g, A, F> TxDescendants<'g, A, F> {
|
||||
let mut descendants = Self {
|
||||
graph,
|
||||
visited: Default::default(),
|
||||
stack: Default::default(),
|
||||
queue: Default::default(),
|
||||
filter_map,
|
||||
};
|
||||
descendants.populate_stack(1, txid);
|
||||
descendants.populate_queue(1, txid);
|
||||
descendants
|
||||
}
|
||||
|
||||
/// Creates a `TxDescendants` from multiple starting transactions that include the starting
|
||||
/// Creates a `TxDescendants` from multiple starting transactions that includes the starting
|
||||
/// `txid`s when iterating.
|
||||
pub(crate) fn from_multiple_include_root<I>(
|
||||
graph: &'g TxGraph<A>,
|
||||
@ -1186,7 +1386,7 @@ impl<'g, A, F> TxDescendants<'g, A, F> {
|
||||
Self {
|
||||
graph,
|
||||
visited: Default::default(),
|
||||
stack: txids.into_iter().map(|txid| (0, txid)).collect(),
|
||||
queue: txids.into_iter().map(|txid| (0, txid)).collect(),
|
||||
filter_map,
|
||||
}
|
||||
}
|
||||
@ -1205,25 +1405,25 @@ impl<'g, A, F> TxDescendants<'g, A, F> {
|
||||
let mut descendants = Self {
|
||||
graph,
|
||||
visited: Default::default(),
|
||||
stack: Default::default(),
|
||||
queue: Default::default(),
|
||||
filter_map,
|
||||
};
|
||||
for txid in txids {
|
||||
descendants.populate_stack(1, txid);
|
||||
descendants.populate_queue(1, txid);
|
||||
}
|
||||
descendants
|
||||
}
|
||||
}
|
||||
|
||||
impl<'g, A, F> TxDescendants<'g, A, F> {
|
||||
fn populate_stack(&mut self, depth: usize, txid: Txid) {
|
||||
fn populate_queue(&mut self, depth: usize, txid: Txid) {
|
||||
let spend_paths = self
|
||||
.graph
|
||||
.spends
|
||||
.range(tx_outpoint_range(txid))
|
||||
.flat_map(|(_, spends)| spends)
|
||||
.map(|&txid| (depth, txid));
|
||||
self.stack.extend(spend_paths);
|
||||
self.queue.extend(spend_paths);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1235,8 +1435,8 @@ where
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let (op_spends, txid, item) = loop {
|
||||
// we have exhausted all paths when stack is empty
|
||||
let (op_spends, txid) = self.stack.pop()?;
|
||||
// we have exhausted all paths when queue is empty
|
||||
let (op_spends, txid) = self.queue.pop_front()?;
|
||||
// we do not want to visit the same transaction twice
|
||||
if self.visited.insert(txid) {
|
||||
// ignore paths when user filters them out
|
||||
@ -1246,7 +1446,7 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
self.populate_stack(op_spends + 1, txid);
|
||||
self.populate_queue(op_spends + 1, txid);
|
||||
Some(item)
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,16 @@
|
||||
mod tx_template;
|
||||
pub use tx_template::*;
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! block_id {
|
||||
($height:expr, $hash:literal) => {{
|
||||
bdk_chain::BlockId {
|
||||
height: $height,
|
||||
hash: bitcoin::hashes::Hash::hash($hash.as_bytes()),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! h {
|
||||
($index:literal) => {{
|
||||
|
136
crates/chain/tests/common/tx_template.rs
Normal file
136
crates/chain/tests/common/tx_template.rs
Normal file
@ -0,0 +1,136 @@
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bdk_chain::{tx_graph::TxGraph, BlockId, SpkTxOutIndex};
|
||||
use bitcoin::{
|
||||
locktime::absolute::LockTime, secp256k1::Secp256k1, OutPoint, ScriptBuf, Sequence, Transaction,
|
||||
TxIn, TxOut, Txid, Witness,
|
||||
};
|
||||
use miniscript::Descriptor;
|
||||
|
||||
/// Template for creating a transaction in `TxGraph`.
|
||||
///
|
||||
/// The incentive for transaction templates is to create a transaction history in a simple manner to
|
||||
/// avoid having to explicitly hash previous transactions to form previous outpoints of later
|
||||
/// transactions.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct TxTemplate<'a, A> {
|
||||
/// Uniquely identifies the transaction, before it can have a txid.
|
||||
pub tx_name: &'a str,
|
||||
pub inputs: &'a [TxInTemplate<'a>],
|
||||
pub outputs: &'a [TxOutTemplate],
|
||||
pub anchors: &'a [A],
|
||||
pub last_seen: Option<u64>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum TxInTemplate<'a> {
|
||||
/// This will give a random txid and vout.
|
||||
Bogus,
|
||||
|
||||
/// This is used for coinbase transactions because they do not have previous outputs.
|
||||
Coinbase,
|
||||
|
||||
/// Contains the `tx_name` and `vout` that we are spending. The rule is that we must only spend
|
||||
/// from tx of a previous `TxTemplate`.
|
||||
PrevTx(&'a str, usize),
|
||||
}
|
||||
|
||||
pub struct TxOutTemplate {
|
||||
pub value: u64,
|
||||
pub spk_index: Option<u32>, // some = get spk from SpkTxOutIndex, none = random spk
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl TxOutTemplate {
|
||||
pub fn new(value: u64, spk_index: Option<u32>) -> Self {
|
||||
TxOutTemplate { value, spk_index }
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn init_graph<'a>(
|
||||
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, BlockId>>,
|
||||
) -> (TxGraph<BlockId>, SpkTxOutIndex<u32>, HashMap<&'a str, Txid>) {
|
||||
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
|
||||
let mut graph = TxGraph::<BlockId>::default();
|
||||
let mut spk_index = SpkTxOutIndex::default();
|
||||
(0..10).for_each(|index| {
|
||||
spk_index.insert_spk(
|
||||
index,
|
||||
descriptor
|
||||
.at_derivation_index(index)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
);
|
||||
});
|
||||
let mut tx_ids = HashMap::<&'a str, Txid>::new();
|
||||
|
||||
for (bogus_txin_vout, tx_tmp) in tx_templates.into_iter().enumerate() {
|
||||
let tx = Transaction {
|
||||
version: 0,
|
||||
lock_time: LockTime::ZERO,
|
||||
input: tx_tmp
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|input| match input {
|
||||
TxInTemplate::Bogus => TxIn {
|
||||
previous_output: OutPoint::new(
|
||||
bitcoin::hashes::Hash::hash(
|
||||
Alphanumeric
|
||||
.sample_string(&mut rand::thread_rng(), 20)
|
||||
.as_bytes(),
|
||||
),
|
||||
bogus_txin_vout as u32,
|
||||
),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence::default(),
|
||||
witness: Witness::new(),
|
||||
},
|
||||
TxInTemplate::Coinbase => TxIn {
|
||||
previous_output: OutPoint::null(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence::MAX,
|
||||
witness: Witness::new(),
|
||||
},
|
||||
TxInTemplate::PrevTx(prev_name, prev_vout) => {
|
||||
let prev_txid = tx_ids.get(prev_name).expect(
|
||||
"txin template must spend from tx of template that comes before",
|
||||
);
|
||||
TxIn {
|
||||
previous_output: OutPoint::new(*prev_txid, *prev_vout as _),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence::default(),
|
||||
witness: Witness::new(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
output: tx_tmp
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|output| match &output.spk_index {
|
||||
None => TxOut {
|
||||
value: output.value,
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
},
|
||||
Some(index) => TxOut {
|
||||
value: output.value,
|
||||
script_pubkey: spk_index.spk_at_index(index).unwrap().to_owned(),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
tx_ids.insert(tx_tmp.tx_name, tx.txid());
|
||||
spk_index.scan(&tx);
|
||||
let _ = graph.insert_tx(tx.clone());
|
||||
for anchor in tx_tmp.anchors.iter() {
|
||||
let _ = graph.insert_anchor(tx.txid(), *anchor);
|
||||
}
|
||||
if let Some(seen_at) = tx_tmp.last_seen {
|
||||
let _ = graph.insert_seen_at(tx.txid(), seen_at);
|
||||
}
|
||||
}
|
||||
(graph, spk_index, tx_ids)
|
||||
}
|
@ -5,7 +5,7 @@ use bdk_chain::{
|
||||
collections::*,
|
||||
local_chain::LocalChain,
|
||||
tx_graph::{ChangeSet, TxGraph},
|
||||
Anchor, Append, BlockId, ChainPosition, ConfirmationHeightAnchor,
|
||||
Anchor, Append, BlockId, ChainOracle, ChainPosition, ConfirmationHeightAnchor,
|
||||
};
|
||||
use bitcoin::{
|
||||
absolute, hashes::Hash, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid,
|
||||
@ -496,6 +496,208 @@ fn test_calculate_fee_on_coinbase() {
|
||||
assert_eq!(graph.calculate_fee(&tx), Ok(0));
|
||||
}
|
||||
|
||||
// `test_walk_ancestors` uses the following transaction structure:
|
||||
//
|
||||
// a0
|
||||
// / \
|
||||
// b0 b1 b2
|
||||
// / \ \ /
|
||||
// c0 c1 c2 c3
|
||||
// / \ /
|
||||
// d0 d1
|
||||
// \
|
||||
// e0
|
||||
//
|
||||
// where b0 and b1 spend a0, c0 and c1 spend b0, d0 spends c1, etc.
|
||||
#[test]
|
||||
fn test_walk_ancestors() {
|
||||
let local_chain: LocalChain = (0..=20)
|
||||
.map(|ht| (ht, BlockHash::hash(format!("Block Hash {}", ht).as_bytes())))
|
||||
.collect::<BTreeMap<u32, BlockHash>>()
|
||||
.into();
|
||||
let tip = local_chain.tip().expect("must have tip");
|
||||
|
||||
let tx_a0 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(h!("op0"), 0),
|
||||
..TxIn::default()
|
||||
}],
|
||||
output: vec![TxOut::default(), TxOut::default()],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx_b0 spends tx_a0
|
||||
let tx_b0 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_a0.txid(), 0),
|
||||
..TxIn::default()
|
||||
}],
|
||||
output: vec![TxOut::default(), TxOut::default()],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx_b1 spends tx_a0
|
||||
let tx_b1 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_a0.txid(), 1),
|
||||
..TxIn::default()
|
||||
}],
|
||||
output: vec![TxOut::default()],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
let tx_b2 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(h!("op1"), 0),
|
||||
..TxIn::default()
|
||||
}],
|
||||
output: vec![TxOut::default()],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx_c0 spends tx_b0
|
||||
let tx_c0 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_b0.txid(), 0),
|
||||
..TxIn::default()
|
||||
}],
|
||||
output: vec![TxOut::default()],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx_c1 spends tx_b0
|
||||
let tx_c1 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_b0.txid(), 1),
|
||||
..TxIn::default()
|
||||
}],
|
||||
output: vec![TxOut::default()],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx_c2 spends tx_b1 and tx_b2
|
||||
let tx_c2 = Transaction {
|
||||
input: vec![
|
||||
TxIn {
|
||||
previous_output: OutPoint::new(tx_b1.txid(), 0),
|
||||
..TxIn::default()
|
||||
},
|
||||
TxIn {
|
||||
previous_output: OutPoint::new(tx_b2.txid(), 0),
|
||||
..TxIn::default()
|
||||
},
|
||||
],
|
||||
output: vec![TxOut::default()],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
let tx_c3 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(h!("op2"), 0),
|
||||
..TxIn::default()
|
||||
}],
|
||||
output: vec![TxOut::default()],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx_d0 spends tx_c1
|
||||
let tx_d0 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_c1.txid(), 0),
|
||||
..TxIn::default()
|
||||
}],
|
||||
output: vec![TxOut::default()],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx_d1 spends tx_c2 and tx_c3
|
||||
let tx_d1 = Transaction {
|
||||
input: vec![
|
||||
TxIn {
|
||||
previous_output: OutPoint::new(tx_c2.txid(), 0),
|
||||
..TxIn::default()
|
||||
},
|
||||
TxIn {
|
||||
previous_output: OutPoint::new(tx_c3.txid(), 0),
|
||||
..TxIn::default()
|
||||
},
|
||||
],
|
||||
output: vec![TxOut::default()],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx_e0 spends tx_d1
|
||||
let tx_e0 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_d1.txid(), 0),
|
||||
..TxIn::default()
|
||||
}],
|
||||
output: vec![TxOut::default()],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
let mut graph = TxGraph::<BlockId>::new(vec![
|
||||
tx_a0.clone(),
|
||||
tx_b0.clone(),
|
||||
tx_b1.clone(),
|
||||
tx_b2.clone(),
|
||||
tx_c0.clone(),
|
||||
tx_c1.clone(),
|
||||
tx_c2.clone(),
|
||||
tx_c3.clone(),
|
||||
tx_d0.clone(),
|
||||
tx_d1.clone(),
|
||||
tx_e0.clone(),
|
||||
]);
|
||||
|
||||
[&tx_a0, &tx_b1].iter().for_each(|&tx| {
|
||||
let _ = graph.insert_anchor(tx.txid(), tip.block_id());
|
||||
});
|
||||
|
||||
let ancestors = [
|
||||
graph
|
||||
.walk_ancestors(&tx_c0, |depth, tx| Some((depth, tx)))
|
||||
.collect::<Vec<_>>(),
|
||||
graph
|
||||
.walk_ancestors(&tx_d0, |depth, tx| Some((depth, tx)))
|
||||
.collect::<Vec<_>>(),
|
||||
graph
|
||||
.walk_ancestors(&tx_e0, |depth, tx| Some((depth, tx)))
|
||||
.collect::<Vec<_>>(),
|
||||
// Only traverse unconfirmed ancestors of tx_e0 this time
|
||||
graph
|
||||
.walk_ancestors(&tx_e0, |depth, tx| {
|
||||
let tx_node = graph.get_tx_node(tx.txid())?;
|
||||
for block in tx_node.anchors {
|
||||
match local_chain.is_block_in_chain(block.anchor_block(), tip.block_id()) {
|
||||
Ok(Some(true)) => return None,
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
Some((depth, tx_node.tx))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
];
|
||||
|
||||
let expected_ancestors = [
|
||||
vec![(1, &tx_b0), (2, &tx_a0)],
|
||||
vec![(1, &tx_c1), (2, &tx_b0), (3, &tx_a0)],
|
||||
vec![
|
||||
(1, &tx_d1),
|
||||
(2, &tx_c2),
|
||||
(2, &tx_c3),
|
||||
(3, &tx_b1),
|
||||
(3, &tx_b2),
|
||||
(4, &tx_a0),
|
||||
],
|
||||
vec![(1, &tx_d1), (2, &tx_c2), (2, &tx_c3), (3, &tx_b2)],
|
||||
];
|
||||
|
||||
for (txids, expected_txids) in ancestors.iter().zip(expected_ancestors.iter()) {
|
||||
assert_eq!(txids, expected_txids);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conflicting_descendants() {
|
||||
let previous_output = OutPoint::new(h!("op"), 2);
|
||||
@ -610,7 +812,7 @@ fn test_descendants_no_repeat() {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut graph = TxGraph::<()>::default();
|
||||
let mut expected_txids = BTreeSet::new();
|
||||
let mut expected_txids = Vec::new();
|
||||
|
||||
// these are NOT descendants of `tx_a`
|
||||
for tx in txs_not_connected {
|
||||
@ -625,19 +827,14 @@ fn test_descendants_no_repeat() {
|
||||
.chain(core::iter::once(&tx_e))
|
||||
{
|
||||
let _ = graph.insert_tx(tx.clone());
|
||||
assert!(expected_txids.insert(tx.txid()));
|
||||
expected_txids.push(tx.txid());
|
||||
}
|
||||
|
||||
let descendants = graph
|
||||
.walk_descendants(tx_a.txid(), |_, txid| Some(txid))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(descendants.len(), expected_txids.len());
|
||||
|
||||
for txid in descendants {
|
||||
assert!(expected_txids.remove(&txid));
|
||||
}
|
||||
assert!(expected_txids.is_empty());
|
||||
assert_eq!(descendants, expected_txids);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
629
crates/chain/tests/test_tx_graph_conflicts.rs
Normal file
629
crates/chain/tests/test_tx_graph_conflicts.rs
Normal file
@ -0,0 +1,629 @@
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
|
||||
use bdk_chain::{keychain::Balance, BlockId};
|
||||
use bitcoin::{OutPoint, Script};
|
||||
use common::*;
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct Scenario<'a> {
|
||||
/// Name of the test scenario
|
||||
name: &'a str,
|
||||
/// Transaction templates
|
||||
tx_templates: &'a [TxTemplate<'a, BlockId>],
|
||||
/// Names of txs that must exist in the output of `list_chain_txs`
|
||||
exp_chain_txs: HashSet<&'a str>,
|
||||
/// Outpoints that must exist in the output of `filter_chain_txouts`
|
||||
exp_chain_txouts: HashSet<(&'a str, u32)>,
|
||||
/// Outpoints of UTXOs that must exist in the output of `filter_chain_unspents`
|
||||
exp_unspents: HashSet<(&'a str, u32)>,
|
||||
/// Expected balances
|
||||
exp_balance: Balance,
|
||||
}
|
||||
|
||||
/// This test ensures that [`TxGraph`] will reliably filter out irrelevant transactions when
|
||||
/// presented with multiple conflicting transaction scenarios using the [`TxTemplate`] structure.
|
||||
/// This test also checks that [`TxGraph::list_chain_txs`], [`TxGraph::filter_chain_txouts`],
|
||||
/// [`TxGraph::filter_chain_unspents`], and [`TxGraph::balance`] return correct data.
|
||||
#[test]
|
||||
fn test_tx_conflict_handling() {
|
||||
// Create Local chains
|
||||
let local_chain = local_chain!(
|
||||
(0, h!("A")),
|
||||
(1, h!("B")),
|
||||
(2, h!("C")),
|
||||
(3, h!("D")),
|
||||
(4, h!("E")),
|
||||
(5, h!("F")),
|
||||
(6, h!("G"))
|
||||
);
|
||||
let chain_tip = local_chain
|
||||
.tip()
|
||||
.map(|cp| cp.block_id())
|
||||
.unwrap_or_default();
|
||||
|
||||
let scenarios = [
|
||||
Scenario {
|
||||
name: "2 unconfirmed txs with same last_seens conflict",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "tx1",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(40000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_1",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(2))],
|
||||
last_seen: Some(300),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_2",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(3))],
|
||||
last_seen: Some(300),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// correct output if filtered by fee rate: tx1, tx_conflict_1
|
||||
exp_chain_txs: HashSet::from(["tx1", "tx_conflict_1", "tx_conflict_2"]),
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_1", 0), ("tx_conflict_2", 0)]),
|
||||
// correct output if filtered by fee rate: tx_conflict_1
|
||||
exp_unspents: HashSet::from([("tx_conflict_1", 0), ("tx_conflict_2", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 50000, // correct output if filtered by fee rate: 20000
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "2 unconfirmed txs with different last_seens conflict",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "tx1",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_1",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(2))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_2",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::PrevTx("tx1", 1)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(3))],
|
||||
last_seen: Some(300),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
exp_chain_txs: HashSet::from(["tx1", "tx_conflict_2"]),
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx1", 1), ("tx_conflict_2", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "3 unconfirmed txs with different last_seens conflict",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "tx1",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_1",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_2",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(2))],
|
||||
last_seen: Some(300),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_3",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(40000, Some(3))],
|
||||
last_seen: Some(400),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
exp_chain_txs: HashSet::from(["tx1", "tx_conflict_3"]),
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_3", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_3", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 40000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "unconfirmed tx conflicts with tx in orphaned block, orphaned higher last_seen",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "tx1",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_1",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_orphaned_conflict",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(2))],
|
||||
anchors: &[block_id!(4, "Orphaned Block")],
|
||||
last_seen: Some(300),
|
||||
},
|
||||
],
|
||||
exp_chain_txs: HashSet::from(["tx1", "tx_orphaned_conflict"]),
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_orphaned_conflict", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_orphaned_conflict", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "unconfirmed tx conflicts with tx in orphaned block, orphaned lower last_seen",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "tx1",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_1",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_orphaned_conflict",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(2))],
|
||||
anchors: &[block_id!(4, "Orphaned Block")],
|
||||
last_seen: Some(100),
|
||||
},
|
||||
],
|
||||
exp_chain_txs: HashSet::from(["tx1", "tx_conflict_1"]),
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_1", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_1", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 20000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "multiple unconfirmed txs conflict with a confirmed tx",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "tx1",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_1",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_2",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(2))],
|
||||
last_seen: Some(300),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_3",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(40000, Some(3))],
|
||||
last_seen: Some(400),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_confirmed_conflict",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(50000, Some(4))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
exp_chain_txs: HashSet::from(["tx1", "tx_confirmed_conflict"]),
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_confirmed_conflict", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_confirmed_conflict", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 50000,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "B and B' spend A and conflict, C spends B, all the transactions are unconfirmed, B' has higher last_seen than B",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "A",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
last_seen: Some(22),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(23),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B'",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(2))],
|
||||
last_seen: Some(24),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "C",
|
||||
inputs: &[TxInTemplate::PrevTx("B", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(3))],
|
||||
last_seen: Some(25),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// A, B, C will appear in the list methods
|
||||
// This is because B' has a higher last seen than B, but C has a higher
|
||||
// last seen than B', so B and C are considered canonical
|
||||
exp_chain_txs: HashSet::from(["A", "B", "C"]),
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B", 0), ("C", 0)]),
|
||||
exp_unspents: HashSet::from([("C", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "B and B' spend A and conflict, C spends B, A and B' are in best chain",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "A",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B'",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(2))],
|
||||
anchors: &[block_id!(4, "E")],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "C",
|
||||
inputs: &[TxInTemplate::PrevTx("B", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(3))],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// B and C should not appear in the list methods
|
||||
exp_chain_txs: HashSet::from(["A", "B'"]),
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 20000,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "B and B' spend A and conflict, C spends B', A and B' are in best chain",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "A",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B'",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(2))],
|
||||
anchors: &[block_id!(4, "E")],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "C",
|
||||
inputs: &[TxInTemplate::PrevTx("B'", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(3))],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// B should not appear in the list methods
|
||||
exp_chain_txs: HashSet::from(["A", "B'", "C"]),
|
||||
exp_chain_txouts: HashSet::from([
|
||||
("A", 0),
|
||||
("B'", 0),
|
||||
("C", 0),
|
||||
]),
|
||||
exp_unspents: HashSet::from([("C", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "B and B' spend A and conflict, C spends both B and B', A is in best chain",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "A",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B'",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(2))],
|
||||
last_seen: Some(300),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "C",
|
||||
inputs: &[
|
||||
TxInTemplate::PrevTx("B", 0),
|
||||
TxInTemplate::PrevTx("B'", 0),
|
||||
],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(3))],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// C should not appear in the list methods
|
||||
exp_chain_txs: HashSet::from(["A", "B'"]),
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "B and B' spend A and conflict, B' is confirmed, C spends both B and B', A is in best chain",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "A",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B'",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(50000, Some(4))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "C",
|
||||
inputs: &[
|
||||
TxInTemplate::PrevTx("B", 0),
|
||||
TxInTemplate::PrevTx("B'", 0),
|
||||
],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(5))],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// C should not appear in the list methods
|
||||
exp_chain_txs: HashSet::from(["A", "B'"]),
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 50000,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "B and B' spend A and conflict, B' is confirmed, C spends both B and B', D spends C, A is in best chain",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "A",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B'",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(50000, Some(4))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "C",
|
||||
inputs: &[
|
||||
TxInTemplate::PrevTx("B", 0),
|
||||
TxInTemplate::PrevTx("B'", 0),
|
||||
],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(5))],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "D",
|
||||
inputs: &[TxInTemplate::PrevTx("C", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(6))],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// D should not appear in the list methods
|
||||
exp_chain_txs: HashSet::from(["A", "B'"]),
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 50000,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for scenario in scenarios {
|
||||
let (tx_graph, spk_index, exp_tx_ids) = init_graph(scenario.tx_templates.iter());
|
||||
|
||||
let txs = tx_graph
|
||||
.list_chain_txs(&local_chain, chain_tip)
|
||||
.map(|tx| tx.tx_node.txid)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let exp_txs = scenario
|
||||
.exp_chain_txs
|
||||
.iter()
|
||||
.map(|txid| *exp_tx_ids.get(txid).expect("txid must exist"))
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
txs, exp_txs,
|
||||
"\n[{}] 'list_chain_txs' failed",
|
||||
scenario.name
|
||||
);
|
||||
|
||||
let txouts = tx_graph
|
||||
.filter_chain_txouts(
|
||||
&local_chain,
|
||||
chain_tip,
|
||||
spk_index.outpoints().iter().cloned(),
|
||||
)
|
||||
.map(|(_, full_txout)| full_txout.outpoint)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let exp_txouts = scenario
|
||||
.exp_chain_txouts
|
||||
.iter()
|
||||
.map(|(txid, vout)| OutPoint {
|
||||
txid: *exp_tx_ids.get(txid).expect("txid must exist"),
|
||||
vout: *vout,
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
txouts, exp_txouts,
|
||||
"\n[{}] 'filter_chain_txouts' failed",
|
||||
scenario.name
|
||||
);
|
||||
|
||||
let utxos = tx_graph
|
||||
.filter_chain_unspents(
|
||||
&local_chain,
|
||||
chain_tip,
|
||||
spk_index.outpoints().iter().cloned(),
|
||||
)
|
||||
.map(|(_, full_txout)| full_txout.outpoint)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let exp_utxos = scenario
|
||||
.exp_unspents
|
||||
.iter()
|
||||
.map(|(txid, vout)| OutPoint {
|
||||
txid: *exp_tx_ids.get(txid).expect("txid must exist"),
|
||||
vout: *vout,
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
utxos, exp_utxos,
|
||||
"\n[{}] 'filter_chain_unspents' failed",
|
||||
scenario.name
|
||||
);
|
||||
|
||||
let balance = tx_graph.balance(
|
||||
&local_chain,
|
||||
chain_tip,
|
||||
spk_index.outpoints().iter().cloned(),
|
||||
|_, spk: &Script| spk_index.index_of_spk(spk).is_some(),
|
||||
);
|
||||
assert_eq!(
|
||||
balance, scenario.exp_balance,
|
||||
"\n[{}] 'balance' failed",
|
||||
scenario.name
|
||||
);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user