If `BlockId` implements `Anchor`, the meaning is ambiguous. We cannot tell whether it means the tx is anchors at the block, or whether it also means the tx is confirmed at that block. Instead, `ConfirmationHeightAnchor` and `ConfirmationTimeAnchor` structs are introduced as non-ambiguous `Anchor` implementations. Additionally, `TxGraph::relevant_heights` is removed because it is also ambiguous. What heights are deemed relevant? A simpler and more flexible method `TxGraph::all_anchors` is introduced instead.
448 lines
14 KiB
Rust
448 lines
14 KiB
Rust
#[macro_use]
|
|
mod common;
|
|
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
use bdk_chain::{
|
|
indexed_tx_graph::{IndexedAdditions, IndexedTxGraph},
|
|
keychain::{Balance, DerivationAdditions, KeychainTxOutIndex},
|
|
local_chain::LocalChain,
|
|
tx_graph::Additions,
|
|
ConfirmationHeightAnchor, ObservedAs,
|
|
};
|
|
use bitcoin::{secp256k1::Secp256k1, BlockHash, OutPoint, Script, Transaction, TxIn, TxOut};
|
|
use miniscript::Descriptor;
|
|
|
|
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
|
|
/// in topological order.
|
|
///
|
|
/// Given 3 transactions (A, B, C), where A has 2 owned outputs. B and C spends an output each of A.
|
|
/// Typically, we would only know whether B and C are relevant if we have indexed A (A's outpoints
|
|
/// are associated with owned spks in the index). Ensure insertion and indexing is topological-
|
|
/// agnostic.
|
|
#[test]
|
|
fn insert_relevant_txs() {
|
|
const DESCRIPTOR: &str = "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)";
|
|
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTOR)
|
|
.expect("must be valid");
|
|
let spk_0 = descriptor.at_derivation_index(0).script_pubkey();
|
|
let spk_1 = descriptor.at_derivation_index(9).script_pubkey();
|
|
|
|
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::default();
|
|
graph.index.add_keychain((), descriptor);
|
|
graph.index.set_lookahead(&(), 10);
|
|
|
|
let tx_a = Transaction {
|
|
output: vec![
|
|
TxOut {
|
|
value: 10_000,
|
|
script_pubkey: spk_0,
|
|
},
|
|
TxOut {
|
|
value: 20_000,
|
|
script_pubkey: spk_1,
|
|
},
|
|
],
|
|
..common::new_tx(0)
|
|
};
|
|
|
|
let tx_b = Transaction {
|
|
input: vec![TxIn {
|
|
previous_output: OutPoint::new(tx_a.txid(), 0),
|
|
..Default::default()
|
|
}],
|
|
..common::new_tx(1)
|
|
};
|
|
|
|
let tx_c = Transaction {
|
|
input: vec![TxIn {
|
|
previous_output: OutPoint::new(tx_a.txid(), 1),
|
|
..Default::default()
|
|
}],
|
|
..common::new_tx(2)
|
|
};
|
|
|
|
let txs = [tx_c, tx_b, tx_a];
|
|
|
|
assert_eq!(
|
|
graph.insert_relevant_txs(txs.iter().map(|tx| (tx, None)), None),
|
|
IndexedAdditions {
|
|
graph_additions: Additions {
|
|
tx: txs.into(),
|
|
..Default::default()
|
|
},
|
|
index_additions: DerivationAdditions([((), 9_u32)].into()),
|
|
}
|
|
)
|
|
}
|
|
|
|
#[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() {
|
|
// 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::<ConfirmationHeightAnchor, 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());
|
|
untrusted_spks.push(script.clone());
|
|
}
|
|
}
|
|
|
|
// Create test transactions
|
|
|
|
// tx1 is the genesis coinbase
|
|
let tx1 = Transaction {
|
|
input: vec![TxIn {
|
|
previous_output: OutPoint::null(),
|
|
..Default::default()
|
|
}],
|
|
output: vec![TxOut {
|
|
value: 70000,
|
|
script_pubkey: trusted_spks[0].clone(),
|
|
}],
|
|
..common::new_tx(0)
|
|
};
|
|
|
|
// tx2 is an incoming transaction received at untrusted keychain at block 1.
|
|
let tx2 = Transaction {
|
|
output: vec![TxOut {
|
|
value: 30000,
|
|
script_pubkey: untrusted_spks[0].clone(),
|
|
}],
|
|
..common::new_tx(0)
|
|
};
|
|
|
|
// tx3 spends tx2 and gives a change back in trusted keychain. Confirmed at Block 2.
|
|
let tx3 = Transaction {
|
|
input: vec![TxIn {
|
|
previous_output: OutPoint::new(tx2.txid(), 0),
|
|
..Default::default()
|
|
}],
|
|
output: vec![TxOut {
|
|
value: 10000,
|
|
script_pubkey: trusted_spks[1].clone(),
|
|
}],
|
|
..common::new_tx(0)
|
|
};
|
|
|
|
// tx4 is an external transaction receiving at untrusted keychain, unconfirmed.
|
|
let tx4 = Transaction {
|
|
output: vec![TxOut {
|
|
value: 20000,
|
|
script_pubkey: untrusted_spks[1].clone(),
|
|
}],
|
|
..common::new_tx(0)
|
|
};
|
|
|
|
// tx5 is spending tx3 and receiving change at trusted keychain, unconfirmed.
|
|
let tx5 = Transaction {
|
|
output: vec![TxOut {
|
|
value: 15000,
|
|
script_pubkey: trusted_spks[2].clone(),
|
|
}],
|
|
..common::new_tx(0)
|
|
};
|
|
|
|
// 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().enumerate().map(|(i, tx)| {
|
|
(
|
|
*tx,
|
|
local_chain
|
|
.get_block(i as u32)
|
|
.map(|anchor_block| ConfirmationHeightAnchor {
|
|
anchor_block,
|
|
confirmation_height: anchor_block.height,
|
|
}),
|
|
)
|
|
}),
|
|
None,
|
|
);
|
|
|
|
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.
|
|
let fetch =
|
|
|ht: u32, graph: &IndexedTxGraph<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>| {
|
|
let txouts = graph
|
|
.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(ht).unwrap())
|
|
.collect::<Vec<_>>();
|
|
|
|
let balance = graph.balance(
|
|
&local_chain,
|
|
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| {
|
|
if matches!(full_txout.chain_position, ObservedAs::Confirmed(_)) {
|
|
Some(full_txout.outpoint.txid)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<BTreeSet<_>>();
|
|
|
|
let unconfirmed_txouts_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<_>>();
|
|
|
|
(
|
|
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_txouts_txid,
|
|
[tx2.txid(), tx3.txid(), tx4.txid(), tx5.txid()].into()
|
|
);
|
|
|
|
assert_eq!(confirmed_utxos_txid, [tx1.txid()].into());
|
|
assert_eq!(
|
|
unconfirmed_utxos_txid,
|
|
[tx3.txid(), tx4.txid(), tx5.txid()].into()
|
|
);
|
|
|
|
assert_eq!(
|
|
balance,
|
|
Balance {
|
|
immature: 70000, // immature coinbase
|
|
trusted_pending: 25000, // tx3 + tx5
|
|
untrusted_pending: 20000, // tx4
|
|
confirmed: 0 // Nothing is confirmed yet
|
|
}
|
|
);
|
|
}
|
|
|
|
// AT Block 1
|
|
{
|
|
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_txouts_txid,
|
|
[tx3.txid(), tx4.txid(), tx5.txid()].into()
|
|
);
|
|
|
|
// tx2 doesn't get into confirmed utxos set
|
|
assert_eq!(confirmed_utxos_txid, [tx1.txid()].into());
|
|
assert_eq!(
|
|
unconfirmed_utxos_txid,
|
|
[tx3.txid(), tx4.txid(), tx5.txid()].into()
|
|
);
|
|
|
|
assert_eq!(
|
|
balance,
|
|
Balance {
|
|
immature: 70000, // immature coinbase
|
|
trusted_pending: 25000, // tx3 + tx5
|
|
untrusted_pending: 20000, // tx4
|
|
confirmed: 0 // Nothing is confirmed yet
|
|
}
|
|
);
|
|
}
|
|
|
|
// AT Block 2
|
|
{
|
|
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_txouts_txid, [tx4.txid(), tx5.txid()].into());
|
|
|
|
// tx3 also gets into confirmed utxo set
|
|
assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into());
|
|
assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into());
|
|
|
|
assert_eq!(
|
|
balance,
|
|
Balance {
|
|
immature: 70000, // immature coinbase
|
|
trusted_pending: 15000, // tx5
|
|
untrusted_pending: 20000, // tx4
|
|
confirmed: 10000 // tx3 got confirmed
|
|
}
|
|
);
|
|
}
|
|
|
|
// AT Block 98
|
|
{
|
|
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_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, // coinbase matured
|
|
trusted_pending: 15000, // tx5
|
|
untrusted_pending: 20000, // tx4
|
|
confirmed: 80000 // tx1 + tx3
|
|
}
|
|
);
|
|
}
|
|
}
|