use bdk_chain::{
indexed_tx_graph::{IndexedAdditions, IndexedTxGraph},
keychain::{Balance, DerivationAdditions, KeychainTxOutIndex},
+ local_chain::LocalChain,
tx_graph::Additions,
BlockId, ObservedAs,
};
}
#[test]
+/// Ensure consistency IndexedTxGraph list_* and balance methods. These methods lists
+/// relevant txouts and utxos from the information fetched from a ChainOracle (here a LocalChain).
+///
+/// Test Setup:
+///
+/// Local Chain => <0> ----- <1> ----- <2> ----- <3> ---- ... ---- <150>
+///
+/// Keychains:
+///
+/// keychain_1: Trusted
+/// keychain_2: Untrusted
+///
+/// Transactions:
+///
+/// tx1: A Coinbase, sending 70000 sats to "trusted" address. [Block 0]
+/// tx2: A external Receive, sending 30000 sats to "untrusted" address. [Block 1]
+/// tx3: Internal Spend. Spends tx2 and returns change of 10000 to "trusted" address. [Block 2]
+/// tx4: Mempool tx, sending 20000 sats to "trusted" address.
+/// tx5: Mempool tx, sending 15000 sats to "untested" address.
+/// tx6: Complete unrelated tx. [Block 3]
+///
+/// Different transactions are added via `insert_relevant_txs`.
+/// `list_owned_txout`, `list_owned_utxos` and `balance` method is asserted
+/// with expected values at Block height 0, 1, and 2.
+///
+/// Finally Add more blocks to local chain until tx1 coinbase maturity hits.
+/// Assert maturity at coinbase maturity inflection height. Block height 98 and 99.
+
fn test_list_owned_txouts() {
- let mut local_chain = local_chain![
- (0, h!("Block 0")),
- (1, h!("Block 1")),
- (2, h!("Block 2")),
- (3, h!("Block 3"))
- ];
-
- let desc_1 : &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)";
- let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), desc_1).unwrap();
- let desc_2 : &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)";
- let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), desc_2).unwrap();
+ // Create Local chains
+
+ let local_chain = (0..150)
+ .map(|i| (i as u32, h!("random")))
+ .collect::<BTreeMap<u32, BlockHash>>();
+ let local_chain = LocalChain::from(local_chain);
+
+ // Initiate IndexedTxGraph
+
+ 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 mut graph = IndexedTxGraph::<BlockId, KeychainTxOutIndex<String>>::default();
graph.index.add_keychain("keychain_1".into(), desc_1);
graph.index.add_keychain("keychain_2".into(), desc_2);
-
graph.index.set_lookahead_for_all(10);
+ // Get trusted and untrusted addresses
+
let mut trusted_spks = Vec::new();
let mut untrusted_spks = Vec::new();
{
+ // we need to scope here to take immutanble reference of the graph
for _ in 0..10 {
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_1".to_string());
+ // TODO Assert indexes
trusted_spks.push(script.clone());
}
}
-
{
for _ in 0..10 {
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_2".to_string());
}
}
- let trust_predicate = |spk: &Script| trusted_spks.contains(spk);
+ // Create test transactions
- // tx1 is coinbase transaction received at trusted keychain at block 0.
+ // tx1 is the genesis coinbase
let tx1 = Transaction {
input: vec![TxIn {
previous_output: OutPoint::null(),
// tx6 is an unrelated transaction confirmed at 3.
let tx6 = common::new_tx(0);
+ // Insert transactions into graph with respective anchors
+ // For unconfirmed txs we pass in `None`.
+
let _ = graph.insert_relevant_txs(
[&tx1, &tx2, &tx3, &tx6]
.iter()
let _ = graph.insert_relevant_txs([&tx4, &tx5].iter().map(|tx| (*tx, None)), Some(100));
- // AT Block 0
- {
+ // A helper lambda to extract and filter data from the graph.
+ let fetch = |ht: u32, graph: &IndexedTxGraph<BlockId, KeychainTxOutIndex<String>>| {
let txouts = graph
- .list_owned_txouts(&local_chain, local_chain.get_block(0).unwrap())
+ .list_owned_txouts(&local_chain, local_chain.get_block(ht).unwrap())
.collect::<Vec<_>>();
let utxos = graph
- .list_owned_unspents(&local_chain, local_chain.get_block(0).unwrap())
+ .list_owned_unspents(&local_chain, local_chain.get_block(ht).unwrap())
.collect::<Vec<_>>();
let balance = graph.balance(
&local_chain,
- local_chain.get_block(0).unwrap(),
- trust_predicate,
+ local_chain.get_block(ht).unwrap(),
+ |spk: &Script| trusted_spks.contains(spk),
);
+ assert_eq!(txouts.len(), 5);
+ assert_eq!(utxos.len(), 4);
+
let confirmed_txouts_txid = txouts
.iter()
.filter_map(|full_txout| {
})
.collect::<BTreeSet<_>>();
- let unconfirmed_txout_txid = txouts
+ let unconfirmed_txouts_txid = txouts
.iter()
.filter_map(|full_txout| {
if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
})
.collect::<BTreeSet<_>>();
- assert_eq!(txouts.len(), 5);
- assert_eq!(utxos.len(), 4);
+ (
+ confirmed_txouts_txid,
+ unconfirmed_txouts_txid,
+ confirmed_utxos_txid,
+ unconfirmed_utxos_txid,
+ balance,
+ )
+ };
+
+ // ----- TEST BLOCK -----
+
+ // AT Block 0
+ {
+ let (
+ confirmed_txouts_txid,
+ unconfirmed_txouts_txid,
+ confirmed_utxos_txid,
+ unconfirmed_utxos_txid,
+ balance,
+ ) = fetch(0, &graph);
assert_eq!(confirmed_txouts_txid, [tx1.txid()].into());
assert_eq!(
- unconfirmed_txout_txid,
+ unconfirmed_txouts_txid,
[tx2.txid(), tx3.txid(), tx4.txid(), tx5.txid()].into()
);
// AT Block 1
{
- let txouts = graph
- .list_owned_txouts(&local_chain, local_chain.get_block(1).unwrap())
- .collect::<Vec<_>>();
-
- let utxos = graph
- .list_owned_unspents(&local_chain, local_chain.get_block(1).unwrap())
- .collect::<Vec<_>>();
-
- let balance = graph.balance(
- &local_chain,
- local_chain.get_block(1).unwrap(),
- trust_predicate,
- );
-
- let confirmed_txouts_txid = txouts
- .iter()
- .filter_map(|full_txout| {
- if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
- Some(full_txout.outpoint.txid)
- } else {
- None
- }
- })
- .collect::<BTreeSet<_>>();
-
- let unconfirmed_txout_txid = txouts
- .iter()
- .filter_map(|full_txout| {
- if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
- Some(full_txout.outpoint.txid)
- } else {
- None
- }
- })
- .collect::<BTreeSet<_>>();
-
- let confirmed_utxos_txid = utxos
- .iter()
- .filter_map(|full_txout| {
- if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
- Some(full_txout.outpoint.txid)
- } else {
- None
- }
- })
- .collect::<BTreeSet<_>>();
-
- let unconfirmed_utxos_txid = utxos
- .iter()
- .filter_map(|full_txout| {
- if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
- Some(full_txout.outpoint.txid)
- } else {
- None
- }
- })
- .collect::<BTreeSet<_>>();
-
- assert_eq!(txouts.len(), 5);
- assert_eq!(utxos.len(), 4);
+ let (
+ confirmed_txouts_txid,
+ unconfirmed_txouts_txid,
+ confirmed_utxos_txid,
+ unconfirmed_utxos_txid,
+ balance,
+ ) = fetch(1, &graph);
// tx2 gets into confirmed txout set
assert_eq!(confirmed_txouts_txid, [tx1.txid(), tx2.txid()].into());
assert_eq!(
- unconfirmed_txout_txid,
+ unconfirmed_txouts_txid,
[tx3.txid(), tx4.txid(), tx5.txid()].into()
);
[tx3.txid(), tx4.txid(), tx5.txid()].into()
);
- // Balance breakup remains same
assert_eq!(
balance,
Balance {
// AT Block 2
{
- let txouts = graph
- .list_owned_txouts(&local_chain, local_chain.get_block(2).unwrap())
- .collect::<Vec<_>>();
-
- let utxos = graph
- .list_owned_unspents(&local_chain, local_chain.get_block(2).unwrap())
- .collect::<Vec<_>>();
-
- let balance = graph.balance(
- &local_chain,
- local_chain.get_block(2).unwrap(),
- trust_predicate,
- );
-
- let confirmed_txouts_txid = txouts
- .iter()
- .filter_map(|full_txout| {
- if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
- Some(full_txout.outpoint.txid)
- } else {
- None
- }
- })
- .collect::<BTreeSet<_>>();
-
- let unconfirmed_txout_txid = txouts
- .iter()
- .filter_map(|full_txout| {
- if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
- Some(full_txout.outpoint.txid)
- } else {
- None
- }
- })
- .collect::<BTreeSet<_>>();
-
- let confirmed_utxos_txid = utxos
- .iter()
- .filter_map(|full_txout| {
- if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
- Some(full_txout.outpoint.txid)
- } else {
- None
- }
- })
- .collect::<BTreeSet<_>>();
-
- let unconfirmed_utxos_txid = utxos
- .iter()
- .filter_map(|full_txout| {
- if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
- Some(full_txout.outpoint.txid)
- } else {
- None
- }
- })
- .collect::<BTreeSet<_>>();
-
- assert_eq!(txouts.len(), 5);
- assert_eq!(utxos.len(), 4);
+ let (
+ confirmed_txouts_txid,
+ unconfirmed_txouts_txid,
+ confirmed_utxos_txid,
+ unconfirmed_utxos_txid,
+ balance,
+ ) = fetch(2, &graph);
// tx3 now gets into the confirmed txout set
assert_eq!(
confirmed_txouts_txid,
[tx1.txid(), tx2.txid(), tx3.txid()].into()
);
- assert_eq!(unconfirmed_txout_txid, [tx4.txid(), tx5.txid()].into());
+ assert_eq!(unconfirmed_txouts_txid, [tx4.txid(), tx5.txid()].into());
// tx3 also gets into confirmed utxo set
assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into());
);
}
- // AT Block 110
+ // AT Block 98
{
- let mut local_chain_extension = (4..150)
- .map(|i| (i as u32, h!("random")))
- .collect::<BTreeMap<u32, BlockHash>>();
-
- local_chain_extension.insert(3, h!("Block 3"));
-
- local_chain
- .apply_update(local_chain_extension.into())
- .unwrap();
-
- let txouts = graph
- .list_owned_txouts(&local_chain, local_chain.get_block(110).unwrap())
- .collect::<Vec<_>>();
-
- let utxos = graph
- .list_owned_unspents(&local_chain, local_chain.get_block(110).unwrap())
- .collect::<Vec<_>>();
-
- let balance = graph.balance(
- &local_chain,
- local_chain.get_block(110).unwrap(),
- trust_predicate,
- );
-
- let confirmed_txouts_txid = txouts
- .iter()
- .filter_map(|full_txout| {
- if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
- Some(full_txout.outpoint.txid)
- } else {
- None
- }
- })
- .collect::<BTreeSet<_>>();
-
- let unconfirmed_txout_txid = txouts
- .iter()
- .filter_map(|full_txout| {
- if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
- Some(full_txout.outpoint.txid)
- } else {
- None
- }
- })
- .collect::<BTreeSet<_>>();
-
- let confirmed_utxos_txid = utxos
- .iter()
- .filter_map(|full_txout| {
- if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
- Some(full_txout.outpoint.txid)
- } else {
- None
- }
- })
- .collect::<BTreeSet<_>>();
-
- let unconfirmed_utxos_txid = utxos
- .iter()
- .filter_map(|full_txout| {
- if matches!(full_txout.chain_position, ObservedAs::Unconfirmed(_)) {
- Some(full_txout.outpoint.txid)
- } else {
- None
- }
- })
- .collect::<BTreeSet<_>>();
-
- println!("TxOuts : {:#?}", txouts);
- println!("UTXOS {:#?}", utxos);
- println!("{:#?}", balance);
-
- assert_eq!(txouts.len(), 5);
- assert_eq!(utxos.len(), 4);
+ let (
+ confirmed_txouts_txid,
+ unconfirmed_txouts_txid,
+ confirmed_utxos_txid,
+ unconfirmed_utxos_txid,
+ balance,
+ ) = fetch(98, &graph);
assert_eq!(
confirmed_txouts_txid,
[tx1.txid(), tx2.txid(), tx3.txid()].into()
);
- assert_eq!(unconfirmed_txout_txid, [tx4.txid(), tx5.txid()].into());
+ assert_eq!(unconfirmed_txouts_txid, [tx4.txid(), tx5.txid()].into());
assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into());
assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into());
+ // Coinbase is still immature
+ assert_eq!(
+ balance,
+ Balance {
+ immature: 70000, // immature coinbase
+ trusted_pending: 15000, // tx5
+ untrusted_pending: 20000, // tx4
+ confirmed: 10000 // tx1 got matured
+ }
+ );
+ }
+
+ // AT Block 99
+ {
+ let (_, _, _, _, balance) = fetch(100, &graph);
+
+ // Coinbase maturity hits
assert_eq!(
balance,
Balance {
- immature: 0, // immature coinbase
+ immature: 0, // coinbase matured
trusted_pending: 15000, // tx5
untrusted_pending: 20000, // tx4
- confirmed: 80000 // tx1 got matured
+ confirmed: 80000 // tx1 + tx3
}
);
}