mod common;
use bdk_chain::{
collections::*,
+ local_chain::LocalChain,
tx_graph::{Additions, TxGraph},
+ BlockId, ObservedAs,
+};
+use bitcoin::{
+ hashes::Hash, BlockHash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid,
};
-use bitcoin::{hashes::Hash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid};
use core::iter;
#[test]
fn insert_txouts() {
+ // 2 (Outpoint, TxOut) tupples that denotes original data in the graph, as partial transactions.
let original_ops = [
(
OutPoint::new(h!("tx1"), 1),
),
];
+ // Another (OutPoint, TxOut) tupple to be used as update as partial transaction.
let update_ops = [(
OutPoint::new(h!("tx2"), 0),
TxOut {
},
)];
+ // One full transaction to be included in the update
+ let update_txs = Transaction {
+ version: 0x01,
+ lock_time: PackedLockTime(0),
+ input: vec![TxIn {
+ previous_output: OutPoint::null(),
+ ..Default::default()
+ }],
+ output: vec![TxOut {
+ value: 30_000,
+ script_pubkey: Script::new(),
+ }],
+ };
+
+ // Conf anchor used to mark the full transaction as confirmed.
+ let conf_anchor = ObservedAs::Confirmed(BlockId {
+ height: 100,
+ hash: h!("random blockhash"),
+ });
+
+ // Unconfirmed anchor to mark the partial transactions as unconfirmed
+ let unconf_anchor = ObservedAs::<BlockId>::Unconfirmed(1000000);
+
+ // Make the original graph
let mut graph = {
- let mut graph = TxGraph::<()>::default();
+ let mut graph = TxGraph::<ObservedAs<BlockId>>::default();
for (outpoint, txout) in &original_ops {
assert_eq!(
graph.insert_txout(*outpoint, txout.clone()),
graph
};
+ // Make the update graph
let update = {
let mut graph = TxGraph::default();
for (outpoint, txout) in &update_ops {
+ // Insert partials transactions
assert_eq!(
graph.insert_txout(*outpoint, txout.clone()),
Additions {
..Default::default()
}
);
+ // Mark them unconfirmed.
+ assert_eq!(
+ graph.insert_anchor(outpoint.txid, unconf_anchor),
+ Additions {
+ tx: [].into(),
+ txout: [].into(),
+ anchors: [(unconf_anchor, outpoint.txid)].into(),
+ last_seen: [].into()
+ }
+ );
+ // Mark them last seen at.
+ assert_eq!(
+ graph.insert_seen_at(outpoint.txid, 1000000),
+ Additions {
+ tx: [].into(),
+ txout: [].into(),
+ anchors: [].into(),
+ last_seen: [(outpoint.txid, 1000000)].into()
+ }
+ );
}
+ // Insert the full transaction
+ assert_eq!(
+ graph.insert_tx(update_txs.clone()),
+ Additions {
+ tx: [update_txs.clone()].into(),
+ ..Default::default()
+ }
+ );
+
+ // Mark it as confirmed.
+ assert_eq!(
+ graph.insert_anchor(update_txs.txid(), conf_anchor),
+ Additions {
+ tx: [].into(),
+ txout: [].into(),
+ anchors: [(conf_anchor, update_txs.txid())].into(),
+ last_seen: [].into()
+ }
+ );
graph
};
+ // Check the resulting addition.
let additions = graph.determine_additions(&update);
assert_eq!(
additions,
Additions {
- tx: [].into(),
+ tx: [update_txs.clone()].into(),
txout: update_ops.into(),
- ..Default::default()
+ anchors: [(conf_anchor, update_txs.txid()), (unconf_anchor, h!("tx2"))].into(),
+ last_seen: [(h!("tx2"), 1000000)].into()
}
);
+ // Apply addition and check the new graph counts.
graph.apply_additions(additions);
- assert_eq!(graph.all_txouts().count(), 3);
- assert_eq!(graph.full_transactions().count(), 0);
+ assert_eq!(graph.all_txouts().count(), 4);
+ assert_eq!(graph.full_transactions().count(), 1);
assert_eq!(graph.partial_transactions().count(), 2);
+
+ // Check TxOuts are fetched correctly from the graph.
+ assert_eq!(
+ graph.txouts(h!("tx1")).expect("should exists"),
+ [
+ (
+ 1u32,
+ &TxOut {
+ value: 10_000,
+ script_pubkey: Script::new(),
+ }
+ ),
+ (
+ 2u32,
+ &TxOut {
+ value: 20_000,
+ script_pubkey: Script::new(),
+ }
+ )
+ ]
+ .into()
+ );
+
+ assert_eq!(
+ graph.txouts(update_txs.txid()).expect("should exists"),
+ [(
+ 0u32,
+ &TxOut {
+ value: 30_000,
+ script_pubkey: Script::new()
+ }
+ )]
+ .into()
+ );
}
#[test]
}
assert!(expected_txids.is_empty());
}
+
+#[test]
+fn test_chain_spends() {
+ let local_chain: LocalChain = (0..=100)
+ .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");
+
+ // The parent tx contains 2 outputs. Which are spent by one confirmed and one unconfirmed tx.
+ // The parent tx is confirmed at block 95.
+ let tx_0 = Transaction {
+ input: vec![],
+ output: vec![
+ TxOut {
+ value: 10_000,
+ script_pubkey: Script::new(),
+ },
+ TxOut {
+ value: 20_000,
+ script_pubkey: Script::new(),
+ },
+ ],
+ ..common::new_tx(0)
+ };
+
+ // The first confirmed transaction spends vout: 0. And is confirmed at block 98.
+ let tx_1 = Transaction {
+ input: vec![TxIn {
+ previous_output: OutPoint::new(tx_0.txid(), 0),
+ ..TxIn::default()
+ }],
+ output: vec![
+ TxOut {
+ value: 5_000,
+ script_pubkey: Script::new(),
+ },
+ TxOut {
+ value: 5_000,
+ script_pubkey: Script::new(),
+ },
+ ],
+ ..common::new_tx(0)
+ };
+
+ // The second transactions spends vout:1, and is unconfirmed.
+ let tx_2 = Transaction {
+ input: vec![TxIn {
+ previous_output: OutPoint::new(tx_0.txid(), 1),
+ ..TxIn::default()
+ }],
+ output: vec![
+ TxOut {
+ value: 10_000,
+ script_pubkey: Script::new(),
+ },
+ TxOut {
+ value: 10_000,
+ script_pubkey: Script::new(),
+ },
+ ],
+ ..common::new_tx(0)
+ };
+
+ let mut graph = TxGraph::<BlockId>::default();
+
+ let _ = graph.insert_tx(tx_0.clone());
+ let _ = graph.insert_tx(tx_1.clone());
+ let _ = graph.insert_tx(tx_2.clone());
+
+ [95, 98]
+ .iter()
+ .zip([&tx_0, &tx_1].into_iter())
+ .for_each(|(ht, tx)| {
+ let block_id = local_chain.get_block(*ht).expect("block expected");
+ let _ = graph.insert_anchor(tx.txid(), block_id);
+ });
+
+ // Assert that confirmed spends are returned correctly.
+ assert_eq!(
+ graph
+ .get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 0))
+ .unwrap(),
+ (
+ ObservedAs::Confirmed(&local_chain.get_block(98).expect("block expected")),
+ tx_1.txid()
+ )
+ );
+
+ // Check if chain position is returned correctly.
+ assert_eq!(
+ graph
+ .get_chain_position(&local_chain, tip, tx_0.txid())
+ .expect("position expected"),
+ ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))
+ );
+
+ // As long the unconfirmed tx isn't marked as seen, chain_spend will return None.
+ assert!(graph
+ .get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1))
+ .is_none());
+
+ // Mark the unconfirmed as seen and check correct ObservedAs status is returned.
+ let _ = graph.insert_seen_at(tx_2.txid(), 1234567);
+
+ // Check chain spend returned correctly.
+ assert_eq!(
+ graph
+ .get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1))
+ .unwrap(),
+ (ObservedAs::Unconfirmed(1234567), tx_2.txid())
+ );
+
+ // A conflicting transaction that conflicts with tx_1.
+ let tx_1_conflict = Transaction {
+ input: vec![TxIn {
+ previous_output: OutPoint::new(tx_0.txid(), 0),
+ ..Default::default()
+ }],
+ ..common::new_tx(0)
+ };
+ let _ = graph.insert_tx(tx_1_conflict.clone());
+
+ // Because this tx conflicts with an already confirmed transaction, chain position should return none.
+ assert!(graph
+ .get_chain_position(&local_chain, tip, tx_1_conflict.txid())
+ .is_none());
+
+ // Another conflicting tx that conflicts with tx_2.
+ let tx_2_conflict = Transaction {
+ input: vec![TxIn {
+ previous_output: OutPoint::new(tx_0.txid(), 1),
+ ..Default::default()
+ }],
+ ..common::new_tx(0)
+ };
+
+ // Insert in graph and mark it as seen.
+ let _ = graph.insert_tx(tx_2_conflict.clone());
+ let _ = graph.insert_seen_at(tx_2_conflict.txid(), 1234568);
+
+ // This should return a valid observation with correct last seen.
+ assert_eq!(
+ graph
+ .get_chain_position(&local_chain, tip, tx_2_conflict.txid())
+ .expect("position expected"),
+ ObservedAs::Unconfirmed(1234568)
+ );
+
+ // Chain_spend now catches the new transaction as the spend.
+ assert_eq!(
+ graph
+ .get_chain_spend(&local_chain, tip, OutPoint::new(tx_0.txid(), 1))
+ .expect("expect observation"),
+ (ObservedAs::Unconfirmed(1234568), tx_2_conflict.txid())
+ );
+
+ // Chain position of the `tx_2` is now none, as it is older than `tx_2_conflict`
+ assert!(graph
+ .get_chain_position(&local_chain, tip, tx_2.txid())
+ .is_none());
+}