From 001efdd1cb658f3a2929ffcf9047e6b07e9bd15c Mon Sep 17 00:00:00 2001 From: rajarshimaitra Date: Sat, 8 Apr 2023 21:02:33 +0530 Subject: [PATCH] Include tests for new updates of TxGraph --- crates/chain/src/local_chain.rs | 7 + crates/chain/tests/test_tx_graph.rs | 282 +++++++++++++++++++++++++++- 2 files changed, 283 insertions(+), 6 deletions(-) diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 58cf923b..e3385e1b 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -67,6 +67,13 @@ impl LocalChain { .map(|(&height, &hash)| BlockId { height, hash }) } + /// Get a block at the given height. + pub fn get_block(&self, height: u32) -> Option { + self.blocks + .get(&height) + .map(|&hash| BlockId { height, hash }) + } + /// This is like the sparsechain's logic, expect we must guarantee that all invalidated heights /// are to be re-filled. pub fn determine_changeset(&self, update: &Self) -> Result { diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 279ddb74..e3bc0ac8 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -2,13 +2,18 @@ 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), @@ -26,6 +31,7 @@ fn insert_txouts() { ), ]; + // Another (OutPoint, TxOut) tupple to be used as update as partial transaction. let update_ops = [( OutPoint::new(h!("tx2"), 0), TxOut { @@ -34,8 +40,32 @@ fn insert_txouts() { }, )]; + // 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::::Unconfirmed(1000000); + + // Make the original graph let mut graph = { - let mut graph = TxGraph::<()>::default(); + let mut graph = TxGraph::>::default(); for (outpoint, txout) in &original_ops { assert_eq!( graph.insert_txout(*outpoint, txout.clone()), @@ -48,9 +78,11 @@ fn insert_txouts() { 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 { @@ -58,25 +90,101 @@ fn insert_txouts() { ..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] @@ -511,3 +619,165 @@ fn test_descendants_no_repeat() { } 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::>() + .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::::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()); +}