Move bdk_chain into the bdk repo 🎉

Original repository: 250b4f1dcc/bdk_chain

Co-authored-by: Steve Myers <steve@notmandatory.org>
Co-authored-by: 志宇 <hello@evanlinjin.me>
Co-authored-by: LLFourn <lloyd.fourn@gmail.com>
Co-authored-by: rajarshimaitra <rajarshi149@gmail.com>
Co-authored-by: LagginTimes <wzc110@gmail.com>
Co-authored-by: Steve Myers <steve@notmandatory.org>
Co-authored-by: Vladimir Fomene <vladimirfomene@gmail.com>
This commit is contained in:
Daniela Brozzoni
2023-03-01 11:09:08 +01:00
parent f2188f9dcd
commit 37dfa77d9d
22 changed files with 7061 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
#[allow(unused_macros)]
macro_rules! h {
($index:literal) => {{
bitcoin::hashes::Hash::hash($index.as_bytes())
}};
}
#[allow(unused_macros)]
macro_rules! chain {
($([$($tt:tt)*]),*) => { chain!( checkpoints: [$([$($tt)*]),*] ) };
(checkpoints: $($tail:tt)*) => { chain!( index: TxHeight, checkpoints: $($tail)*) };
(index: $ind:ty, checkpoints: [ $([$height:expr, $block_hash:expr]),* ] $(,txids: [$(($txid:expr, $tx_height:expr)),*])?) => {{
#[allow(unused_mut)]
let mut chain = bdk_chain::sparse_chain::SparseChain::<$ind>::from_checkpoints([$(($height, $block_hash).into()),*]);
$(
$(
let _ = chain.insert_tx($txid, $tx_height).expect("should succeed");
)*
)?
chain
}};
}
#[allow(unused_macros)]
macro_rules! changeset {
(checkpoints: $($tail:tt)*) => { changeset!(index: TxHeight, checkpoints: $($tail)*) };
(
index: $ind:ty,
checkpoints: [ $(( $height:expr, $cp_to:expr )),* ]
$(,txids: [ $(( $txid:expr, $tx_to:expr )),* ])?
) => {{
use bdk_chain::collections::BTreeMap;
#[allow(unused_mut)]
bdk_chain::sparse_chain::ChangeSet::<$ind> {
checkpoints: {
let mut changes = BTreeMap::default();
$(changes.insert($height, $cp_to);)*
changes
},
txids: {
let mut changes = BTreeMap::default();
$($(changes.insert($txid, $tx_to.map(|h: TxHeight| h.into()));)*)?
changes
}
}
}};
}
#[allow(unused)]
pub fn new_tx(lt: u32) -> bitcoin::Transaction {
bitcoin::Transaction {
version: 0x00,
lock_time: bitcoin::PackedLockTime(lt),
input: vec![],
output: vec![],
}
}

View File

@@ -0,0 +1,653 @@
#[macro_use]
mod common;
use bdk_chain::{
chain_graph::*,
collections::HashSet,
sparse_chain,
tx_graph::{self, TxGraph},
BlockId, TxHeight,
};
use bitcoin::{OutPoint, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Witness};
#[test]
fn test_spent_by() {
let tx1 = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut::default()],
};
let op = OutPoint {
txid: tx1.txid(),
vout: 0,
};
let tx2 = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: op,
..Default::default()
}],
output: vec![],
};
let tx3 = Transaction {
version: 0x01,
lock_time: PackedLockTime(42),
input: vec![TxIn {
previous_output: op,
..Default::default()
}],
output: vec![],
};
let mut cg1 = ChainGraph::default();
let _ = cg1
.insert_tx(tx1, TxHeight::Unconfirmed)
.expect("should insert");
let mut cg2 = cg1.clone();
let _ = cg1
.insert_tx(tx2.clone(), TxHeight::Unconfirmed)
.expect("should insert");
let _ = cg2
.insert_tx(tx3.clone(), TxHeight::Unconfirmed)
.expect("should insert");
assert_eq!(cg1.spent_by(op), Some((&TxHeight::Unconfirmed, tx2.txid())));
assert_eq!(cg2.spent_by(op), Some((&TxHeight::Unconfirmed, tx3.txid())));
}
#[test]
fn update_evicts_conflicting_tx() {
let cp_a = BlockId {
height: 0,
hash: h!("A"),
};
let cp_b = BlockId {
height: 1,
hash: h!("B"),
};
let cp_b2 = BlockId {
height: 1,
hash: h!("B'"),
};
let tx_a = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut::default()],
};
let tx_b = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::new(tx_a.txid(), 0),
script_sig: Script::new(),
sequence: Sequence::default(),
witness: Witness::new(),
}],
output: vec![TxOut::default()],
};
let tx_b2 = Transaction {
version: 0x02,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::new(tx_a.txid(), 0),
script_sig: Script::new(),
sequence: Sequence::default(),
witness: Witness::new(),
}],
output: vec![TxOut::default(), TxOut::default()],
};
{
let mut cg1 = {
let mut cg = ChainGraph::default();
let _ = cg.insert_checkpoint(cp_a).expect("should insert cp");
let _ = cg
.insert_tx(tx_a.clone(), TxHeight::Confirmed(0))
.expect("should insert tx");
let _ = cg
.insert_tx(tx_b.clone(), TxHeight::Unconfirmed)
.expect("should insert tx");
cg
};
let cg2 = {
let mut cg = ChainGraph::default();
let _ = cg
.insert_tx(tx_b2.clone(), TxHeight::Unconfirmed)
.expect("should insert tx");
cg
};
let changeset = ChangeSet::<TxHeight, Transaction> {
chain: sparse_chain::ChangeSet {
checkpoints: Default::default(),
txids: [
(tx_b.txid(), None),
(tx_b2.txid(), Some(TxHeight::Unconfirmed)),
]
.into(),
},
graph: tx_graph::Additions {
tx: [tx_b2.clone()].into(),
txout: [].into(),
},
};
assert_eq!(
cg1.determine_changeset(&cg2),
Ok(changeset.clone()),
"tx should be evicted from mempool"
);
cg1.apply_changeset(changeset);
}
{
let cg1 = {
let mut cg = ChainGraph::default();
let _ = cg.insert_checkpoint(cp_a).expect("should insert cp");
let _ = cg.insert_checkpoint(cp_b).expect("should insert cp");
let _ = cg
.insert_tx(tx_a.clone(), TxHeight::Confirmed(0))
.expect("should insert tx");
let _ = cg
.insert_tx(tx_b.clone(), TxHeight::Confirmed(1))
.expect("should insert tx");
cg
};
let cg2 = {
let mut cg = ChainGraph::default();
let _ = cg
.insert_tx(tx_b2.clone(), TxHeight::Unconfirmed)
.expect("should insert tx");
cg
};
assert_eq!(
cg1.determine_changeset(&cg2),
Err(UpdateError::UnresolvableConflict(UnresolvableConflict {
already_confirmed_tx: (TxHeight::Confirmed(1), tx_b.txid()),
update_tx: (TxHeight::Unconfirmed, tx_b2.txid()),
})),
"fail if tx is evicted from valid block"
);
}
{
// Given 2 blocks `{A, B}`, and an update that invalidates block B with
// `{A, B'}`, we expect txs that exist in `B` that conflicts with txs
// introduced in the update to be successfully evicted.
let mut cg1 = {
let mut cg = ChainGraph::default();
let _ = cg.insert_checkpoint(cp_a).expect("should insert cp");
let _ = cg.insert_checkpoint(cp_b).expect("should insert cp");
let _ = cg
.insert_tx(tx_a.clone(), TxHeight::Confirmed(0))
.expect("should insert tx");
let _ = cg
.insert_tx(tx_b.clone(), TxHeight::Confirmed(1))
.expect("should insert tx");
cg
};
let cg2 = {
let mut cg = ChainGraph::default();
let _ = cg.insert_checkpoint(cp_a).expect("should insert cp");
let _ = cg.insert_checkpoint(cp_b2).expect("should insert cp");
let _ = cg
.insert_tx(tx_b2.clone(), TxHeight::Unconfirmed)
.expect("should insert tx");
cg
};
let changeset = ChangeSet::<TxHeight, Transaction> {
chain: sparse_chain::ChangeSet {
checkpoints: [(1, Some(h!("B'")))].into(),
txids: [
(tx_b.txid(), None),
(tx_b2.txid(), Some(TxHeight::Unconfirmed)),
]
.into(),
},
graph: tx_graph::Additions {
tx: [tx_b2.clone()].into(),
txout: [].into(),
},
};
assert_eq!(
cg1.determine_changeset(&cg2),
Ok(changeset.clone()),
"tx should be evicted from B",
);
cg1.apply_changeset(changeset);
}
}
#[test]
fn chain_graph_new_missing() {
let tx_a = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut::default()],
};
let tx_b = Transaction {
version: 0x02,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut::default()],
};
let update = chain!(
index: TxHeight,
checkpoints: [[0, h!("A")]],
txids: [
(tx_a.txid(), TxHeight::Confirmed(0)),
(tx_b.txid(), TxHeight::Confirmed(0))
]
);
let mut graph = TxGraph::default();
let mut expected_missing = HashSet::new();
expected_missing.insert(tx_a.txid());
expected_missing.insert(tx_b.txid());
assert_eq!(
ChainGraph::new(update.clone(), graph.clone()),
Err(NewError::Missing(expected_missing.clone()))
);
let _ = graph.insert_tx(tx_b.clone());
expected_missing.remove(&tx_b.txid());
assert_eq!(
ChainGraph::new(update.clone(), graph.clone()),
Err(NewError::Missing(expected_missing.clone()))
);
let _ = graph.insert_txout(
OutPoint {
txid: tx_a.txid(),
vout: 0,
},
tx_a.output[0].clone(),
);
assert_eq!(
ChainGraph::new(update.clone(), graph.clone()),
Err(NewError::Missing(expected_missing)),
"inserting an output instead of full tx doesn't satisfy constraint"
);
let _ = graph.insert_tx(tx_a.clone());
let new_graph = ChainGraph::new(update.clone(), graph.clone()).unwrap();
let expected_graph = {
let mut cg = ChainGraph::<TxHeight, Transaction>::default();
let _ = cg
.insert_checkpoint(update.latest_checkpoint().unwrap())
.unwrap();
let _ = cg.insert_tx(tx_a, TxHeight::Confirmed(0)).unwrap();
let _ = cg.insert_tx(tx_b, TxHeight::Confirmed(0)).unwrap();
cg
};
assert_eq!(new_graph, expected_graph);
}
#[test]
fn chain_graph_new_conflicts() {
let tx_a = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut::default()],
};
let tx_b = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::new(tx_a.txid(), 0),
script_sig: Script::new(),
sequence: Sequence::default(),
witness: Witness::new(),
}],
output: vec![TxOut::default()],
};
let tx_b2 = Transaction {
version: 0x02,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::new(tx_a.txid(), 0),
script_sig: Script::new(),
sequence: Sequence::default(),
witness: Witness::new(),
}],
output: vec![TxOut::default(), TxOut::default()],
};
let chain = chain!(
index: TxHeight,
checkpoints: [[5, h!("A")]],
txids: [
(tx_a.txid(), TxHeight::Confirmed(1)),
(tx_b.txid(), TxHeight::Confirmed(2)),
(tx_b2.txid(), TxHeight::Confirmed(3))
]
);
let graph = TxGraph::new([tx_a, tx_b, tx_b2]);
assert!(matches!(
ChainGraph::new(chain, graph),
Err(NewError::Conflict { .. })
));
}
#[test]
fn test_get_tx_in_chain() {
let mut cg = ChainGraph::default();
let tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut::default()],
};
let _ = cg.insert_tx(tx.clone(), TxHeight::Unconfirmed).unwrap();
assert_eq!(
cg.get_tx_in_chain(tx.txid()),
Some((&TxHeight::Unconfirmed, &tx))
);
}
#[test]
fn test_iterate_transactions() {
let mut cg = ChainGraph::default();
let txs = (0..3)
.map(|i| Transaction {
version: i,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut::default()],
})
.collect::<Vec<_>>();
let _ = cg
.insert_checkpoint(BlockId {
height: 1,
hash: h!("A"),
})
.unwrap();
let _ = cg
.insert_tx(txs[0].clone(), TxHeight::Confirmed(1))
.unwrap();
let _ = cg.insert_tx(txs[1].clone(), TxHeight::Unconfirmed).unwrap();
let _ = cg
.insert_tx(txs[2].clone(), TxHeight::Confirmed(0))
.unwrap();
assert_eq!(
cg.transactions_in_chain().collect::<Vec<_>>(),
vec![
(&TxHeight::Confirmed(0), &txs[2]),
(&TxHeight::Confirmed(1), &txs[0]),
(&TxHeight::Unconfirmed, &txs[1]),
]
);
}
/// Start with: block1, block2a, tx1, tx2a
/// Update 1: block2a -> block2b , tx2a -> tx2b
/// Update 2: block2b -> block2c , tx2b -> tx2a
#[test]
fn test_apply_changes_reintroduce_tx() {
let block1 = BlockId {
height: 1,
hash: h!("block 1"),
};
let block2a = BlockId {
height: 2,
hash: h!("block 2a"),
};
let block2b = BlockId {
height: 2,
hash: h!("block 2b"),
};
let block2c = BlockId {
height: 2,
hash: h!("block 2c"),
};
let tx1 = Transaction {
version: 0,
lock_time: PackedLockTime(1),
input: Vec::new(),
output: [TxOut {
value: 1,
script_pubkey: Script::new(),
}]
.into(),
};
let tx2a = Transaction {
version: 0,
lock_time: PackedLockTime('a'.into()),
input: [TxIn {
previous_output: OutPoint::new(tx1.txid(), 0),
..Default::default()
}]
.into(),
output: [TxOut {
value: 0,
..Default::default()
}]
.into(),
};
let tx2b = Transaction {
lock_time: PackedLockTime('b'.into()),
..tx2a.clone()
};
// block1, block2a, tx1, tx2a
let mut cg = {
let mut cg = ChainGraph::default();
let _ = cg.insert_checkpoint(block1).unwrap();
let _ = cg.insert_checkpoint(block2a).unwrap();
let _ = cg.insert_tx(tx1.clone(), TxHeight::Confirmed(1)).unwrap();
let _ = cg.insert_tx(tx2a.clone(), TxHeight::Confirmed(2)).unwrap();
cg
};
// block2a -> block2b , tx2a -> tx2b
let update = {
let mut update = ChainGraph::default();
let _ = update.insert_checkpoint(block1).unwrap();
let _ = update.insert_checkpoint(block2b).unwrap();
let _ = update
.insert_tx(tx2b.clone(), TxHeight::Confirmed(2))
.unwrap();
update
};
assert_eq!(
cg.apply_update(update).expect("should update"),
ChangeSet {
chain: changeset! {
checkpoints: [(2, Some(block2b.hash))],
txids: [(tx2a.txid(), None), (tx2b.txid(), Some(TxHeight::Confirmed(2)))]
},
graph: tx_graph::Additions {
tx: [tx2b.clone()].into(),
..Default::default()
},
}
);
// block2b -> block2c , tx2b -> tx2a
let update = {
let mut update = ChainGraph::default();
let _ = update.insert_checkpoint(block1).unwrap();
let _ = update.insert_checkpoint(block2c).unwrap();
let _ = update
.insert_tx(tx2a.clone(), TxHeight::Confirmed(2))
.unwrap();
update
};
assert_eq!(
cg.apply_update(update).expect("should update"),
ChangeSet {
chain: changeset! {
checkpoints: [(2, Some(block2c.hash))],
txids: [(tx2b.txid(), None), (tx2a.txid(), Some(TxHeight::Confirmed(2)))]
},
..Default::default()
}
);
}
#[test]
fn test_evict_descendants() {
let block_1 = BlockId {
height: 1,
hash: h!("block 1"),
};
let block_2a = BlockId {
height: 2,
hash: h!("block 2 a"),
};
let block_2b = BlockId {
height: 2,
hash: h!("block 2 b"),
};
let tx_1 = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(h!("fake tx"), 0),
..Default::default()
}],
output: vec![TxOut {
value: 10_000,
script_pubkey: Script::new(),
}],
..common::new_tx(1)
};
let tx_2 = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_1.txid(), 0),
..Default::default()
}],
output: vec![
TxOut {
value: 20_000,
script_pubkey: Script::new(),
},
TxOut {
value: 30_000,
script_pubkey: Script::new(),
},
],
..common::new_tx(2)
};
let tx_3 = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_2.txid(), 0),
..Default::default()
}],
output: vec![TxOut {
value: 40_000,
script_pubkey: Script::new(),
}],
..common::new_tx(3)
};
let tx_4 = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_2.txid(), 1),
..Default::default()
}],
output: vec![TxOut {
value: 40_000,
script_pubkey: Script::new(),
}],
..common::new_tx(4)
};
let tx_5 = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_4.txid(), 0),
..Default::default()
}],
output: vec![TxOut {
value: 40_000,
script_pubkey: Script::new(),
}],
..common::new_tx(5)
};
let tx_conflict = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_1.txid(), 0),
..Default::default()
}],
output: vec![TxOut {
value: 12345,
script_pubkey: Script::new(),
}],
..common::new_tx(6)
};
// 1 is spent by 2, 2 is spent by 3 and 4, 4 is spent by 5
let _txid_1 = tx_1.txid();
let txid_2 = tx_2.txid();
let txid_3 = tx_3.txid();
let txid_4 = tx_4.txid();
let txid_5 = tx_5.txid();
// this tx conflicts with 2
let txid_conflict = tx_conflict.txid();
let cg = {
let mut cg = ChainGraph::<TxHeight>::default();
let _ = cg.insert_checkpoint(block_1);
let _ = cg.insert_checkpoint(block_2a);
let _ = cg.insert_tx(tx_1, TxHeight::Confirmed(1));
let _ = cg.insert_tx(tx_2, TxHeight::Confirmed(2));
let _ = cg.insert_tx(tx_3, TxHeight::Confirmed(2));
let _ = cg.insert_tx(tx_4, TxHeight::Confirmed(2));
let _ = cg.insert_tx(tx_5, TxHeight::Confirmed(2));
cg
};
let update = {
let mut cg = ChainGraph::<TxHeight>::default();
let _ = cg.insert_checkpoint(block_1);
let _ = cg.insert_checkpoint(block_2b);
let _ = cg.insert_tx(tx_conflict.clone(), TxHeight::Confirmed(2));
cg
};
assert_eq!(
cg.determine_changeset(&update),
Ok(ChangeSet {
chain: changeset! {
checkpoints: [(2, Some(block_2b.hash))],
txids: [(txid_2, None), (txid_3, None), (txid_4, None), (txid_5, None), (txid_conflict, Some(TxHeight::Confirmed(2)))]
},
graph: tx_graph::Additions {
tx: [tx_conflict.clone()].into(),
..Default::default()
}
})
);
let err = cg
.insert_tx_preview(tx_conflict.clone(), TxHeight::Unconfirmed)
.expect_err("must fail due to conflicts");
assert!(matches!(err, InsertTxError::UnresolvableConflict(_)));
}

View File

@@ -0,0 +1,243 @@
#![cfg(feature = "miniscript")]
#[macro_use]
mod common;
use bdk_chain::{
keychain::{Balance, KeychainTracker},
miniscript::{
bitcoin::{secp256k1::Secp256k1, OutPoint, PackedLockTime, Transaction, TxOut},
Descriptor,
},
BlockId, ConfirmationTime, TxHeight,
};
use bitcoin::TxIn;
#[test]
fn test_insert_tx() {
let mut tracker = KeychainTracker::default();
let secp = Secp256k1::new();
let (descriptor, _) = Descriptor::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
tracker.add_keychain((), descriptor.clone());
let txout = TxOut {
value: 100_000,
script_pubkey: descriptor.at_derivation_index(5).script_pubkey(),
};
let tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![txout],
};
let _ = tracker.txout_index.reveal_to_target(&(), 5);
let changeset = tracker
.insert_tx_preview(tx.clone(), ConfirmationTime::Unconfirmed)
.unwrap();
tracker.apply_changeset(changeset);
assert_eq!(
tracker
.chain_graph()
.transactions_in_chain()
.collect::<Vec<_>>(),
vec![(&ConfirmationTime::Unconfirmed, &tx)]
);
assert_eq!(
tracker
.txout_index
.txouts_of_keychain(&())
.collect::<Vec<_>>(),
vec![(
5,
OutPoint {
txid: tx.txid(),
vout: 0
}
)]
);
}
#[test]
fn test_balance() {
use core::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
enum Keychain {
One,
Two,
}
let mut tracker = KeychainTracker::<Keychain, TxHeight>::default();
let one = Descriptor::from_str("tr([73c5da0a/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0/*)#rg247h69").unwrap();
let two = Descriptor::from_str("tr([73c5da0a/86'/0'/0']xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/1/*)#ju05rz2a").unwrap();
tracker.add_keychain(Keychain::One, one.clone());
tracker.add_keychain(Keychain::Two, two.clone());
let tx1 = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 13_000,
script_pubkey: tracker
.txout_index
.reveal_next_spk(&Keychain::One)
.0
.1
.clone(),
}],
};
let tx2 = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 7_000,
script_pubkey: tracker
.txout_index
.reveal_next_spk(&Keychain::Two)
.0
.1
.clone(),
}],
};
let tx_coinbase = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![TxIn::default()],
output: vec![TxOut {
value: 11_000,
script_pubkey: tracker
.txout_index
.reveal_next_spk(&Keychain::Two)
.0
.1
.clone(),
}],
};
assert!(tx_coinbase.is_coin_base());
let _ = tracker
.insert_checkpoint(BlockId {
height: 5,
hash: h!("1"),
})
.unwrap();
let should_trust = |keychain: &Keychain| match keychain {
&Keychain::One => false,
&Keychain::Two => true,
};
assert_eq!(tracker.balance(should_trust), Balance::default());
let _ = tracker
.insert_tx(tx1.clone(), TxHeight::Unconfirmed)
.unwrap();
assert_eq!(
tracker.balance(should_trust),
Balance {
untrusted_pending: 13_000,
..Default::default()
}
);
let _ = tracker
.insert_tx(tx2.clone(), TxHeight::Unconfirmed)
.unwrap();
assert_eq!(
tracker.balance(should_trust),
Balance {
trusted_pending: 7_000,
untrusted_pending: 13_000,
..Default::default()
}
);
let _ = tracker
.insert_tx(tx_coinbase, TxHeight::Confirmed(0))
.unwrap();
assert_eq!(
tracker.balance(should_trust),
Balance {
trusted_pending: 7_000,
untrusted_pending: 13_000,
immature: 11_000,
..Default::default()
}
);
let _ = tracker
.insert_tx(tx1.clone(), TxHeight::Confirmed(1))
.unwrap();
assert_eq!(
tracker.balance(should_trust),
Balance {
trusted_pending: 7_000,
untrusted_pending: 0,
immature: 11_000,
confirmed: 13_000,
}
);
let _ = tracker
.insert_tx(tx2.clone(), TxHeight::Confirmed(2))
.unwrap();
assert_eq!(
tracker.balance(should_trust),
Balance {
trusted_pending: 0,
untrusted_pending: 0,
immature: 11_000,
confirmed: 20_000,
}
);
let _ = tracker
.insert_checkpoint(BlockId {
height: 98,
hash: h!("98"),
})
.unwrap();
assert_eq!(
tracker.balance(should_trust),
Balance {
trusted_pending: 0,
untrusted_pending: 0,
immature: 11_000,
confirmed: 20_000,
}
);
let _ = tracker
.insert_checkpoint(BlockId {
height: 99,
hash: h!("99"),
})
.unwrap();
assert_eq!(
tracker.balance(should_trust),
Balance {
trusted_pending: 0,
untrusted_pending: 0,
immature: 0,
confirmed: 31_000,
}
);
assert_eq!(tracker.balance_at(0), 0);
assert_eq!(tracker.balance_at(1), 13_000);
assert_eq!(tracker.balance_at(2), 20_000);
assert_eq!(tracker.balance_at(98), 20_000);
assert_eq!(tracker.balance_at(99), 31_000);
assert_eq!(tracker.balance_at(100), 31_000);
}

View File

@@ -0,0 +1,328 @@
#![cfg(feature = "miniscript")]
#[macro_use]
mod common;
use bdk_chain::{
collections::BTreeMap,
keychain::{DerivationAdditions, KeychainTxOutIndex},
};
use bitcoin::{secp256k1::Secp256k1, Script, Transaction, TxOut};
use miniscript::{Descriptor, DescriptorPublicKey};
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
enum TestKeychain {
External,
Internal,
}
fn init_txout_index() -> (
bdk_chain::keychain::KeychainTxOutIndex<TestKeychain>,
Descriptor<DescriptorPublicKey>,
Descriptor<DescriptorPublicKey>,
) {
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<TestKeychain>::default();
let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
txout_index.add_keychain(TestKeychain::External, external_descriptor.clone());
txout_index.add_keychain(TestKeychain::Internal, internal_descriptor.clone());
(txout_index, external_descriptor, internal_descriptor)
}
fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> Script {
descriptor
.derived_descriptor(&Secp256k1::verification_only(), index)
.expect("must derive")
.script_pubkey()
}
#[test]
fn test_set_all_derivation_indices() {
let (mut txout_index, _, _) = init_txout_index();
let derive_to: BTreeMap<_, _> =
[(TestKeychain::External, 12), (TestKeychain::Internal, 24)].into();
assert_eq!(
txout_index.reveal_to_target_multi(&derive_to).1.as_inner(),
&derive_to
);
assert_eq!(txout_index.last_revealed_indices(), &derive_to);
assert_eq!(
txout_index.reveal_to_target_multi(&derive_to).1,
DerivationAdditions::default(),
"no changes if we set to the same thing"
);
}
#[test]
fn test_lookahead() {
let (mut txout_index, external_desc, internal_desc) = init_txout_index();
// ensure it does not break anything if lookahead is set multiple times
(0..=10).for_each(|lookahead| txout_index.set_lookahead(&TestKeychain::External, lookahead));
(0..=20)
.filter(|v| v % 2 == 0)
.for_each(|lookahead| txout_index.set_lookahead(&TestKeychain::Internal, lookahead));
assert_eq!(txout_index.inner().all_spks().len(), 30);
// given:
// - external lookahead set to 10
// - internal lookahead set to 20
// when:
// - set external derivation index to value higher than last, but within the lookahead value
// expect:
// - scripts cached in spk_txout_index should increase correctly
// - stored scripts of external keychain should be of expected counts
for index in (0..20).skip_while(|i| i % 2 == 1) {
let (revealed_spks, revealed_additions) =
txout_index.reveal_to_target(&TestKeychain::External, index);
assert_eq!(
revealed_spks.collect::<Vec<_>>(),
vec![(index, spk_at_index(&external_desc, index))],
);
assert_eq!(
revealed_additions.as_inner(),
&[(TestKeychain::External, index)].into()
);
assert_eq!(
txout_index.inner().all_spks().len(),
10 /* external lookahead */ +
20 /* internal lookahead */ +
index as usize + 1 /* `derived` count */
);
assert_eq!(
txout_index
.revealed_spks_of_keychain(&TestKeychain::External)
.count(),
index as usize + 1,
);
assert_eq!(
txout_index
.revealed_spks_of_keychain(&TestKeychain::Internal)
.count(),
0,
);
assert_eq!(
txout_index
.unused_spks_of_keychain(&TestKeychain::External)
.count(),
index as usize + 1,
);
assert_eq!(
txout_index
.unused_spks_of_keychain(&TestKeychain::Internal)
.count(),
0,
);
}
// given:
// - internal lookahead is 20
// - internal derivation index is `None`
// when:
// - derivation index is set ahead of current derivation index + lookahead
// expect:
// - scripts cached in spk_txout_index should increase correctly, a.k.a. no scripts are skipped
let (revealed_spks, revealed_additions) =
txout_index.reveal_to_target(&TestKeychain::Internal, 24);
assert_eq!(
revealed_spks.collect::<Vec<_>>(),
(0..=24)
.map(|index| (index, spk_at_index(&internal_desc, index)))
.collect::<Vec<_>>(),
);
assert_eq!(
revealed_additions.as_inner(),
&[(TestKeychain::Internal, 24)].into()
);
assert_eq!(
txout_index.inner().all_spks().len(),
10 /* external lookahead */ +
20 /* internal lookahead */ +
20 /* external stored index count */ +
25 /* internal stored index count */
);
assert_eq!(
txout_index
.revealed_spks_of_keychain(&TestKeychain::Internal)
.count(),
25,
);
// ensure derivation indices are expected for each keychain
let last_external_index = txout_index
.last_revealed_index(&TestKeychain::External)
.expect("already derived");
let last_internal_index = txout_index
.last_revealed_index(&TestKeychain::Internal)
.expect("already derived");
assert_eq!(last_external_index, 19);
assert_eq!(last_internal_index, 24);
// when:
// - scanning txouts with spks within stored indexes
// expect:
// - no changes to stored index counts
let external_iter = 0..=last_external_index;
let internal_iter = last_internal_index - last_external_index..=last_internal_index;
for (external_index, internal_index) in external_iter.zip(internal_iter) {
let tx = Transaction {
output: vec![
TxOut {
script_pubkey: external_desc
.at_derivation_index(external_index)
.script_pubkey(),
value: 10_000,
},
TxOut {
script_pubkey: internal_desc
.at_derivation_index(internal_index)
.script_pubkey(),
value: 10_000,
},
],
..common::new_tx(external_index)
};
assert_eq!(txout_index.scan(&tx), DerivationAdditions::default());
assert_eq!(
txout_index.last_revealed_index(&TestKeychain::External),
Some(last_external_index)
);
assert_eq!(
txout_index.last_revealed_index(&TestKeychain::Internal),
Some(last_internal_index)
);
assert_eq!(
txout_index
.revealed_spks_of_keychain(&TestKeychain::External)
.count(),
last_external_index as usize + 1,
);
assert_eq!(
txout_index
.revealed_spks_of_keychain(&TestKeychain::Internal)
.count(),
last_internal_index as usize + 1,
);
}
// when:
// - scanning txouts with spks above last stored index
// expect:
// - cached scripts count should increase as expected
// - last stored index should increase as expected
// TODO!
}
#[test]
fn test_wildcard_derivations() {
let (mut txout_index, external_desc, _) = init_txout_index();
let external_spk_0 = external_desc.at_derivation_index(0).script_pubkey();
let external_spk_16 = external_desc.at_derivation_index(16).script_pubkey();
let external_spk_26 = external_desc.at_derivation_index(26).script_pubkey();
let external_spk_27 = external_desc.at_derivation_index(27).script_pubkey();
// - nothing is derived
// - unused list is also empty
//
// - next_derivation_index() == (0, true)
// - derive_new() == ((0, <spk>), DerivationAdditions)
// - next_unused() == ((0, <spk>), DerivationAdditions:is_empty())
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
assert_eq!(spk, (0_u32, &external_spk_0));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (0_u32, &external_spk_0));
assert_eq!(changeset.as_inner(), &[].into());
// - derived till 25
// - used all spks till 15.
// - used list : [0..=15, 17, 20, 23]
// - unused list: [16, 18, 19, 21, 22, 24, 25]
// - next_derivation_index() = (26, true)
// - derive_new() = ((26, <spk>), DerivationAdditions)
// - next_unused() == ((16, <spk>), DerivationAdditions::is_empty())
let _ = txout_index.reveal_to_target(&TestKeychain::External, 25);
(0..=15)
.into_iter()
.chain([17, 20, 23].into_iter())
.for_each(|index| assert!(txout_index.mark_used(&TestKeychain::External, index)));
assert_eq!(txout_index.next_index(&TestKeychain::External), (26, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
assert_eq!(spk, (26, &external_spk_26));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 26)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (16, &external_spk_16));
assert_eq!(changeset.as_inner(), &[].into());
// - Use all the derived till 26.
// - next_unused() = ((27, <spk>), DerivationAdditions)
(0..=26).into_iter().for_each(|index| {
txout_index.mark_used(&TestKeychain::External, index);
});
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (27, &external_spk_27));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 27)].into());
}
#[test]
fn test_non_wildcard_derivations() {
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::default();
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
let (no_wildcard_descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap();
let external_spk = no_wildcard_descriptor
.at_derivation_index(0)
.script_pubkey();
txout_index.add_keychain(TestKeychain::External, no_wildcard_descriptor);
// given:
// - `txout_index` with no stored scripts
// expect:
// - next derivation index should be new
// - when we derive a new script, script @ index 0
// - when we get the next unused script, script @ index 0
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true));
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
assert_eq!(spk, (0, &external_spk));
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (0, &external_spk));
assert_eq!(changeset.as_inner(), &[].into());
// given:
// - the non-wildcard descriptor already has a stored and used script
// expect:
// - next derivation index should not be new
// - derive new and next unused should return the old script
// - store_up_to should not panic and return empty additions
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, false));
txout_index.mark_used(&TestKeychain::External, 0);
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
assert_eq!(spk, (0, &external_spk));
assert_eq!(changeset.as_inner(), &[].into());
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
assert_eq!(spk, (0, &external_spk));
assert_eq!(changeset.as_inner(), &[].into());
let (revealed_spks, revealed_additions) =
txout_index.reveal_to_target(&TestKeychain::External, 200);
assert_eq!(revealed_spks.count(), 0);
assert!(revealed_additions.is_empty());
}

View File

@@ -0,0 +1,773 @@
#[macro_use]
mod common;
use bdk_chain::{collections::BTreeSet, sparse_chain::*, BlockId, TxHeight};
use bitcoin::{hashes::Hash, Txid};
use core::ops::Bound;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub struct TestIndex(TxHeight, u32);
impl ChainPosition for TestIndex {
fn height(&self) -> TxHeight {
self.0
}
fn max_ord_of_height(height: TxHeight) -> Self {
Self(height, u32::MAX)
}
fn min_ord_of_height(height: TxHeight) -> Self {
Self(height, u32::MIN)
}
}
impl TestIndex {
pub fn new<H>(height: H, ext: u32) -> Self
where
H: Into<TxHeight>,
{
Self(height.into(), ext)
}
}
#[test]
fn add_first_checkpoint() {
let chain = SparseChain::default();
assert_eq!(
chain.determine_changeset(&chain!([0, h!("A")])),
Ok(changeset! {
checkpoints: [(0, Some(h!("A")))],
txids: []
},),
"add first tip"
);
}
#[test]
fn add_second_tip() {
let chain = chain!([0, h!("A")]);
assert_eq!(
chain.determine_changeset(&chain!([0, h!("A")], [1, h!("B")])),
Ok(changeset! {
checkpoints: [(1, Some(h!("B")))],
txids: []
},),
"extend tip by one"
);
}
#[test]
fn two_disjoint_chains_cannot_merge() {
let chain1 = chain!([0, h!("A")]);
let chain2 = chain!([1, h!("B")]);
assert_eq!(
chain1.determine_changeset(&chain2),
Err(UpdateError::NotConnected(0))
);
}
#[test]
fn duplicate_chains_should_merge() {
let chain1 = chain!([0, h!("A")]);
let chain2 = chain!([0, h!("A")]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(ChangeSet::default())
);
}
#[test]
fn duplicate_chains_with_txs_should_merge() {
let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
let chain2 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(ChangeSet::default())
);
}
#[test]
fn duplicate_chains_with_different_txs_should_merge() {
let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
let chain2 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx1"), TxHeight::Confirmed(0))]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [],
txids: [(h!("tx1"), Some(TxHeight::Confirmed(0)))]
})
);
}
#[test]
fn invalidate_first_and_only_checkpoint_without_tx_changes() {
let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
let chain2 = chain!(checkpoints: [[0,h!("A'")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [(0, Some(h!("A'")))],
txids: []
},)
);
}
#[test]
fn invalidate_first_and_only_checkpoint_with_tx_move_forward() {
let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
let chain2 = chain!(checkpoints: [[0,h!("A'")],[1, h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [(0, Some(h!("A'"))), (1, Some(h!("B")))],
txids: [(h!("tx0"), Some(TxHeight::Confirmed(1)))]
},)
);
}
#[test]
fn invalidate_first_and_only_checkpoint_with_tx_move_backward() {
let chain1 = chain!(checkpoints: [[1,h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]);
let chain2 = chain!(checkpoints: [[0,h!("A")],[1, h!("B'")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [(0, Some(h!("A"))), (1, Some(h!("B'")))],
txids: [(h!("tx0"), Some(TxHeight::Confirmed(0)))]
},)
);
}
#[test]
fn invalidate_a_checkpoint_and_try_and_move_tx_when_it_wasnt_within_invalidation() {
let chain1 = chain!(checkpoints: [[0, h!("A")], [1, h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
let chain2 = chain!(checkpoints: [[0, h!("A")], [1, h!("B'")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]);
assert_eq!(
chain1.determine_changeset(&chain2),
Err(UpdateError::TxInconsistent {
txid: h!("tx0"),
original_pos: TxHeight::Confirmed(0).into(),
update_pos: TxHeight::Confirmed(1).into(),
})
);
}
/// This test doesn't make much sense. We're invalidating a block at height 1 and moving it to
/// height 0. It should be impossible for it to be at height 1 at any point if it was at height 0
/// all along.
#[test]
fn move_invalidated_tx_into_earlier_checkpoint() {
let chain1 = chain!(checkpoints: [[0, h!("A")], [1, h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]);
let chain2 = chain!(checkpoints: [[0, h!("A")], [1, h!("B'")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [(1, Some(h!("B'")))],
txids: [(h!("tx0"), Some(TxHeight::Confirmed(0)))]
},)
);
}
#[test]
fn invalidate_first_and_only_checkpoint_with_tx_move_to_mempool() {
let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
let chain2 = chain!(checkpoints: [[0,h!("A'")]], txids: [(h!("tx0"), TxHeight::Unconfirmed)]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [(0, Some(h!("A'")))],
txids: [(h!("tx0"), Some(TxHeight::Unconfirmed))]
},)
);
}
#[test]
fn confirm_tx_without_extending_chain() {
let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Unconfirmed)]);
let chain2 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [],
txids: [(h!("tx0"), Some(TxHeight::Confirmed(0)))]
},)
);
}
#[test]
fn confirm_tx_backwards_while_extending_chain() {
let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Unconfirmed)]);
let chain2 = chain!(checkpoints: [[0,h!("A")],[1,h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(0))]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [(1, Some(h!("B")))],
txids: [(h!("tx0"), Some(TxHeight::Confirmed(0)))]
},)
);
}
#[test]
fn confirm_tx_in_new_block() {
let chain1 = chain!(checkpoints: [[0,h!("A")]], txids: [(h!("tx0"), TxHeight::Unconfirmed)]);
let chain2 = chain! {
checkpoints: [[0,h!("A")], [1,h!("B")]],
txids: [(h!("tx0"), TxHeight::Confirmed(1))]
};
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [(1, Some(h!("B")))],
txids: [(h!("tx0"), Some(TxHeight::Confirmed(1)))]
},)
);
}
#[test]
fn merging_mempool_of_empty_chains_doesnt_fail() {
let chain1 = chain!(checkpoints: [], txids: [(h!("tx0"), TxHeight::Unconfirmed)]);
let chain2 = chain!(checkpoints: [], txids: [(h!("tx1"), TxHeight::Unconfirmed)]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [],
txids: [(h!("tx1"), Some(TxHeight::Unconfirmed))]
},)
);
}
#[test]
fn cannot_insert_confirmed_tx_without_checkpoints() {
let chain = SparseChain::default();
assert_eq!(
chain.insert_tx_preview(h!("A"), TxHeight::Confirmed(0)),
Err(InsertTxError::TxTooHigh {
txid: h!("A"),
tx_height: 0,
tip_height: None
})
);
}
#[test]
fn empty_chain_can_add_unconfirmed_transactions() {
let chain1 = chain!(checkpoints: [[0, h!("A")]], txids: []);
let chain2 = chain!(checkpoints: [], txids: [(h!("tx0"), TxHeight::Unconfirmed)]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [],
txids: [ (h!("tx0"), Some(TxHeight::Unconfirmed)) ]
},)
);
}
#[test]
fn can_update_with_shorter_chain() {
let chain1 = chain!(checkpoints: [[1, h!("B")],[2, h!("C")]], txids: []);
let chain2 = chain!(checkpoints: [[1, h!("B")]], txids: [(h!("tx0"), TxHeight::Confirmed(1))]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [],
txids: [(h!("tx0"), Some(TxHeight::Confirmed(1)))]
},)
)
}
#[test]
fn can_introduce_older_checkpoints() {
let chain1 = chain!(checkpoints: [[2, h!("C")], [3, h!("D")]], txids: []);
let chain2 = chain!(checkpoints: [[1, h!("B")], [2, h!("C")]], txids: []);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [(1, Some(h!("B")))],
txids: []
},)
);
}
#[test]
fn fix_blockhash_before_agreement_point() {
let chain1 = chain!([0, h!("im-wrong")], [1, h!("we-agree")]);
let chain2 = chain!([0, h!("fix")], [1, h!("we-agree")]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [(0, Some(h!("fix")))],
txids: []
},)
)
}
// TODO: Use macro
#[test]
fn cannot_change_ext_index_of_confirmed_tx() {
let chain1 = chain!(
index: TestIndex,
checkpoints: [[1, h!("A")]],
txids: [(h!("tx0"), TestIndex(TxHeight::Confirmed(1), 10))]
);
let chain2 = chain!(
index: TestIndex,
checkpoints: [[1, h!("A")]],
txids: [(h!("tx0"), TestIndex(TxHeight::Confirmed(1), 20))]
);
assert_eq!(
chain1.determine_changeset(&chain2),
Err(UpdateError::TxInconsistent {
txid: h!("tx0"),
original_pos: TestIndex(TxHeight::Confirmed(1), 10),
update_pos: TestIndex(TxHeight::Confirmed(1), 20),
}),
)
}
#[test]
fn can_change_index_of_unconfirmed_tx() {
let chain1 = chain!(
index: TestIndex,
checkpoints: [[1, h!("A")]],
txids: [(h!("tx1"), TestIndex(TxHeight::Unconfirmed, 10))]
);
let chain2 = chain!(
index: TestIndex,
checkpoints: [[1, h!("A")]],
txids: [(h!("tx1"), TestIndex(TxHeight::Unconfirmed, 20))]
);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(ChangeSet {
checkpoints: [].into(),
txids: [(h!("tx1"), Some(TestIndex(TxHeight::Unconfirmed, 20)),)].into()
},),
)
}
/// B and C are in both chain and update
/// ```
/// | 0 | 1 | 2 | 3 | 4
/// chain | B C
/// update | A B C D
/// ```
/// This should succeed with the point of agreement being C and A should be added in addition.
#[test]
fn two_points_of_agreement() {
let chain1 = chain!([1, h!("B")], [2, h!("C")]);
let chain2 = chain!([0, h!("A")], [1, h!("B")], [2, h!("C")], [3, h!("D")]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [(0, Some(h!("A"))), (3, Some(h!("D")))]
},),
);
}
/// Update and chain does not connect:
/// ```
/// | 0 | 1 | 2 | 3 | 4
/// chain | B C
/// update | A B D
/// ```
/// This should fail as we cannot figure out whether C & D are on the same chain
#[test]
fn update_and_chain_does_not_connect() {
let chain1 = chain!([1, h!("B")], [2, h!("C")]);
let chain2 = chain!([0, h!("A")], [1, h!("B")], [3, h!("D")]);
assert_eq!(
chain1.determine_changeset(&chain2),
Err(UpdateError::NotConnected(2)),
);
}
/// Transient invalidation:
/// ```
/// | 0 | 1 | 2 | 3 | 4 | 5
/// chain | A B C E
/// update | A B' C' D
/// ```
/// This should succeed and invalidate B,C and E with point of agreement being A.
/// It should also invalidate transactions at height 1.
#[test]
fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation() {
let chain1 = chain! {
checkpoints: [[0, h!("A")], [2, h!("B")], [3, h!("C")], [5, h!("E")]],
txids: [
(h!("a"), TxHeight::Confirmed(0)),
(h!("b1"), TxHeight::Confirmed(1)),
(h!("b2"), TxHeight::Confirmed(2)),
(h!("d"), TxHeight::Confirmed(3)),
(h!("e"), TxHeight::Confirmed(5))
]
};
let chain2 = chain! {
checkpoints: [[0, h!("A")], [2, h!("B'")], [3, h!("C'")], [4, h!("D")]],
txids: [(h!("b1"), TxHeight::Confirmed(4)), (h!("b2"), TxHeight::Confirmed(3))]
};
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [
(2, Some(h!("B'"))),
(3, Some(h!("C'"))),
(4, Some(h!("D"))),
(5, None)
],
txids: [
(h!("b1"), Some(TxHeight::Confirmed(4))),
(h!("b2"), Some(TxHeight::Confirmed(3))),
(h!("d"), Some(TxHeight::Unconfirmed)),
(h!("e"), Some(TxHeight::Unconfirmed))
]
},)
);
}
/// Transient invalidation:
/// ```
/// | 0 | 1 | 2 | 3 | 4
/// chain | B C E
/// update | B' C' D
/// ```
///
/// This should succeed and invalidate B, C and E with no point of agreement
#[test]
fn transitive_invalidation_applies_to_checkpoints_higher_than_invalidation_no_point_of_agreement() {
let chain1 = chain!([1, h!("B")], [2, h!("C")], [4, h!("E")]);
let chain2 = chain!([1, h!("B'")], [2, h!("C'")], [3, h!("D")]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [
(1, Some(h!("B'"))),
(2, Some(h!("C'"))),
(3, Some(h!("D"))),
(4, None)
]
},)
)
}
/// Transient invalidation:
/// ```
/// | 0 | 1 | 2 | 3 | 4
/// chain | A B C E
/// update | B' C' D
/// ```
///
/// This should fail since although it tells us that B and C are invalid it doesn't tell us whether
/// A was invalid.
#[test]
fn invalidation_but_no_connection() {
let chain1 = chain!([0, h!("A")], [1, h!("B")], [2, h!("C")], [4, h!("E")]);
let chain2 = chain!([1, h!("B'")], [2, h!("C'")], [3, h!("D")]);
assert_eq!(
chain1.determine_changeset(&chain2),
Err(UpdateError::NotConnected(0))
)
}
#[test]
fn checkpoint_limit_is_respected() {
let mut chain1 = SparseChain::default();
let _ = chain1
.apply_update(chain!(
[1, h!("A")],
[2, h!("B")],
[3, h!("C")],
[4, h!("D")],
[5, h!("E")]
))
.unwrap();
assert_eq!(chain1.checkpoints().len(), 5);
chain1.set_checkpoint_limit(Some(4));
assert_eq!(chain1.checkpoints().len(), 4);
let _ = chain1
.insert_checkpoint(BlockId {
height: 6,
hash: h!("F"),
})
.unwrap();
assert_eq!(chain1.checkpoints().len(), 4);
let changeset = chain1.determine_changeset(&chain!([6, h!("F")], [7, h!("G")]));
assert_eq!(changeset, Ok(changeset!(checkpoints: [(7, Some(h!("G")))])));
chain1.apply_changeset(changeset.unwrap());
assert_eq!(chain1.checkpoints().len(), 4);
}
#[test]
fn range_txids_by_height() {
let mut chain = chain!(index: TestIndex, checkpoints: [[1, h!("block 1")], [2, h!("block 2")]]);
let txids: [(TestIndex, Txid); 4] = [
(
TestIndex(TxHeight::Confirmed(1), u32::MIN),
Txid::from_inner([0x00; 32]),
),
(
TestIndex(TxHeight::Confirmed(1), u32::MAX),
Txid::from_inner([0xfe; 32]),
),
(
TestIndex(TxHeight::Confirmed(2), u32::MIN),
Txid::from_inner([0x01; 32]),
),
(
TestIndex(TxHeight::Confirmed(2), u32::MAX),
Txid::from_inner([0xff; 32]),
),
];
// populate chain with txids
for (index, txid) in txids {
let _ = chain.insert_tx(txid, index).expect("should succeed");
}
// inclusive start
assert_eq!(
chain
.range_txids_by_height(TxHeight::Confirmed(1)..)
.collect::<Vec<_>>(),
txids.iter().collect::<Vec<_>>(),
);
// exclusive start
assert_eq!(
chain
.range_txids_by_height((Bound::Excluded(TxHeight::Confirmed(1)), Bound::Unbounded,))
.collect::<Vec<_>>(),
txids[2..].iter().collect::<Vec<_>>(),
);
// inclusive end
assert_eq!(
chain
.range_txids_by_height((Bound::Unbounded, Bound::Included(TxHeight::Confirmed(2))))
.collect::<Vec<_>>(),
txids[..4].iter().collect::<Vec<_>>(),
);
// exclusive end
assert_eq!(
chain
.range_txids_by_height(..TxHeight::Confirmed(2))
.collect::<Vec<_>>(),
txids[..2].iter().collect::<Vec<_>>(),
);
}
#[test]
fn range_txids_by_index() {
let mut chain = chain!(index: TestIndex, checkpoints: [[1, h!("block 1")],[2, h!("block 2")]]);
let txids: [(TestIndex, Txid); 4] = [
(TestIndex(TxHeight::Confirmed(1), u32::MIN), h!("tx 1 min")),
(TestIndex(TxHeight::Confirmed(1), u32::MAX), h!("tx 1 max")),
(TestIndex(TxHeight::Confirmed(2), u32::MIN), h!("tx 2 min")),
(TestIndex(TxHeight::Confirmed(2), u32::MAX), h!("tx 2 max")),
];
// populate chain with txids
for (index, txid) in txids {
let _ = chain.insert_tx(txid, index).expect("should succeed");
}
// inclusive start
assert_eq!(
chain
.range_txids_by_position(TestIndex(TxHeight::Confirmed(1), u32::MIN)..)
.collect::<Vec<_>>(),
txids.iter().collect::<Vec<_>>(),
);
assert_eq!(
chain
.range_txids_by_position(TestIndex(TxHeight::Confirmed(1), u32::MAX)..)
.collect::<Vec<_>>(),
txids[1..].iter().collect::<Vec<_>>(),
);
// exclusive start
assert_eq!(
chain
.range_txids_by_position((
Bound::Excluded(TestIndex(TxHeight::Confirmed(1), u32::MIN)),
Bound::Unbounded
))
.collect::<Vec<_>>(),
txids[1..].iter().collect::<Vec<_>>(),
);
assert_eq!(
chain
.range_txids_by_position((
Bound::Excluded(TestIndex(TxHeight::Confirmed(1), u32::MAX)),
Bound::Unbounded
))
.collect::<Vec<_>>(),
txids[2..].iter().collect::<Vec<_>>(),
);
// inclusive end
assert_eq!(
chain
.range_txids_by_position((
Bound::Unbounded,
Bound::Included(TestIndex(TxHeight::Confirmed(2), u32::MIN))
))
.collect::<Vec<_>>(),
txids[..3].iter().collect::<Vec<_>>(),
);
assert_eq!(
chain
.range_txids_by_position((
Bound::Unbounded,
Bound::Included(TestIndex(TxHeight::Confirmed(2), u32::MAX))
))
.collect::<Vec<_>>(),
txids[..4].iter().collect::<Vec<_>>(),
);
// exclusive end
assert_eq!(
chain
.range_txids_by_position(..TestIndex(TxHeight::Confirmed(2), u32::MIN))
.collect::<Vec<_>>(),
txids[..2].iter().collect::<Vec<_>>(),
);
assert_eq!(
chain
.range_txids_by_position(..TestIndex(TxHeight::Confirmed(2), u32::MAX))
.collect::<Vec<_>>(),
txids[..3].iter().collect::<Vec<_>>(),
);
}
#[test]
fn range_txids() {
let mut chain = SparseChain::default();
let txids = (0..100)
.map(|v| Txid::hash(v.to_string().as_bytes()))
.collect::<BTreeSet<Txid>>();
// populate chain
for txid in &txids {
let _ = chain
.insert_tx(*txid, TxHeight::Unconfirmed)
.expect("should succeed");
}
for txid in &txids {
assert_eq!(
chain
.range_txids((TxHeight::Unconfirmed, *txid)..)
.map(|(_, txid)| txid)
.collect::<Vec<_>>(),
txids.range(*txid..).collect::<Vec<_>>(),
"range with inclusive start should succeed"
);
assert_eq!(
chain
.range_txids((
Bound::Excluded((TxHeight::Unconfirmed, *txid)),
Bound::Unbounded,
))
.map(|(_, txid)| txid)
.collect::<Vec<_>>(),
txids
.range((Bound::Excluded(*txid), Bound::Unbounded,))
.collect::<Vec<_>>(),
"range with exclusive start should succeed"
);
assert_eq!(
chain
.range_txids(..(TxHeight::Unconfirmed, *txid))
.map(|(_, txid)| txid)
.collect::<Vec<_>>(),
txids.range(..*txid).collect::<Vec<_>>(),
"range with exclusive end should succeed"
);
assert_eq!(
chain
.range_txids((
Bound::Included((TxHeight::Unconfirmed, *txid)),
Bound::Unbounded,
))
.map(|(_, txid)| txid)
.collect::<Vec<_>>(),
txids
.range((Bound::Included(*txid), Bound::Unbounded,))
.collect::<Vec<_>>(),
"range with inclusive end should succeed"
);
}
}
#[test]
fn invalidated_txs_move_to_unconfirmed() {
let chain1 = chain! {
checkpoints: [[0, h!("A")], [1, h!("B")], [2, h!("C")]],
txids: [
(h!("a"), TxHeight::Confirmed(0)),
(h!("b"), TxHeight::Confirmed(1)),
(h!("c"), TxHeight::Confirmed(2)),
(h!("d"), TxHeight::Unconfirmed)
]
};
let chain2 = chain!([0, h!("A")], [1, h!("B'")]);
assert_eq!(
chain1.determine_changeset(&chain2),
Ok(changeset! {
checkpoints: [
(1, Some(h!("B'"))),
(2, None)
],
txids: [
(h!("b"), Some(TxHeight::Unconfirmed)),
(h!("c"), Some(TxHeight::Unconfirmed))
]
},)
);
}
#[test]
fn change_tx_position_from_unconfirmed_to_confirmed() {
let mut chain = SparseChain::<TxHeight>::default();
let txid = h!("txid");
let _ = chain.insert_tx(txid, TxHeight::Unconfirmed).unwrap();
assert_eq!(chain.tx_position(txid), Some(&TxHeight::Unconfirmed));
let _ = chain
.insert_checkpoint(BlockId {
height: 0,
hash: h!("0"),
})
.unwrap();
let _ = chain.insert_tx(txid, TxHeight::Confirmed(0)).unwrap();
assert_eq!(chain.tx_position(txid), Some(&TxHeight::Confirmed(0)));
}

View File

@@ -0,0 +1,101 @@
use bdk_chain::SpkTxOutIndex;
use bitcoin::{hashes::hex::FromHex, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut};
#[test]
fn spk_txout_sent_and_received() {
let spk1 = Script::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();
let spk2 = Script::from_hex("00142b57404ae14f08c3a0c903feb2af7830605eb00f").unwrap();
let mut index = SpkTxOutIndex::default();
index.insert_spk(0, spk1.clone());
index.insert_spk(1, spk2.clone());
let tx1 = Transaction {
version: 0x02,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 42_000,
script_pubkey: spk1.clone(),
}],
};
assert_eq!(index.sent_and_received(&tx1), (0, 42_000));
assert_eq!(index.net_value(&tx1), 42_000);
index.scan(&tx1);
assert_eq!(
index.sent_and_received(&tx1),
(0, 42_000),
"shouldn't change after scanning"
);
let tx2 = Transaction {
version: 0x1,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint {
txid: tx1.txid(),
vout: 0,
},
..Default::default()
}],
output: vec![
TxOut {
value: 20_000,
script_pubkey: spk2.clone(),
},
TxOut {
script_pubkey: spk1.clone(),
value: 30_000,
},
],
};
assert_eq!(index.sent_and_received(&tx2), (42_000, 50_000));
assert_eq!(index.net_value(&tx2), 8_000);
}
#[test]
fn mark_used() {
let spk1 = Script::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();
let spk2 = Script::from_hex("00142b57404ae14f08c3a0c903feb2af7830605eb00f").unwrap();
let mut spk_index = SpkTxOutIndex::default();
spk_index.insert_spk(1, spk1.clone());
spk_index.insert_spk(2, spk2.clone());
assert_eq!(spk_index.is_used(&1), false);
spk_index.mark_used(&1);
assert_eq!(spk_index.is_used(&1), true);
spk_index.unmark_used(&1);
assert_eq!(spk_index.is_used(&1), false);
spk_index.mark_used(&1);
assert_eq!(spk_index.is_used(&1), true);
let tx1 = Transaction {
version: 0x02,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 42_000,
script_pubkey: spk1.clone(),
}],
};
spk_index.scan(&tx1);
spk_index.unmark_used(&1);
assert_eq!(
spk_index.is_used(&1),
true,
"even though we unmark_used it doesn't matter because there was a tx scanned that used it"
);
}
#[test]
fn unmark_used_does_not_result_in_invalid_representation() {
let mut spk_index = SpkTxOutIndex::default();
assert_eq!(spk_index.unmark_used(&0), false);
assert_eq!(spk_index.unmark_used(&1), false);
assert_eq!(spk_index.unmark_used(&2), false);
assert!(spk_index.unused_spks(..).collect::<Vec<_>>().is_empty());
}

View File

@@ -0,0 +1,512 @@
#[macro_use]
mod common;
use bdk_chain::{
collections::*,
tx_graph::{Additions, TxGraph},
};
use bitcoin::{hashes::Hash, OutPoint, PackedLockTime, Script, Transaction, TxIn, TxOut, Txid};
use core::iter;
#[test]
fn insert_txouts() {
let original_ops = [
(
OutPoint::new(h!("tx1"), 1),
TxOut {
value: 10_000,
script_pubkey: Script::new(),
},
),
(
OutPoint::new(h!("tx1"), 2),
TxOut {
value: 20_000,
script_pubkey: Script::new(),
},
),
];
let update_ops = [(
OutPoint::new(h!("tx2"), 0),
TxOut {
value: 20_000,
script_pubkey: Script::new(),
},
)];
let mut graph = {
let mut graph = TxGraph::<Transaction>::default();
for (outpoint, txout) in &original_ops {
assert_eq!(
graph.insert_txout(*outpoint, txout.clone()),
Additions {
txout: [(*outpoint, txout.clone())].into(),
..Default::default()
}
);
}
graph
};
let update = {
let mut graph = TxGraph::<Transaction>::default();
for (outpoint, txout) in &update_ops {
assert_eq!(
graph.insert_txout(*outpoint, txout.clone()),
Additions {
txout: [(*outpoint, txout.clone())].into(),
..Default::default()
}
);
}
graph
};
let additions = graph.determine_additions(&update);
assert_eq!(
additions,
Additions {
tx: [].into(),
txout: update_ops.into(),
}
);
graph.apply_additions(additions);
assert_eq!(graph.all_txouts().count(), 3);
assert_eq!(graph.full_transactions().count(), 0);
assert_eq!(graph.partial_transactions().count(), 2);
}
#[test]
fn insert_tx_graph_doesnt_count_coinbase_as_spent() {
let tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::null(),
..Default::default()
}],
output: vec![],
};
let mut graph = TxGraph::default();
let _ = graph.insert_tx(tx);
assert!(graph.outspends(OutPoint::null()).is_empty());
assert!(graph.tx_outspends(Txid::all_zeros()).next().is_none());
}
#[test]
fn insert_tx_graph_keeps_track_of_spend() {
let tx1 = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut::default()],
};
let op = OutPoint {
txid: tx1.txid(),
vout: 0,
};
let tx2 = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: op,
..Default::default()
}],
output: vec![],
};
let mut graph1 = TxGraph::default();
let mut graph2 = TxGraph::default();
// insert in different order
let _ = graph1.insert_tx(tx1.clone());
let _ = graph1.insert_tx(tx2.clone());
let _ = graph2.insert_tx(tx2.clone());
let _ = graph2.insert_tx(tx1.clone());
assert_eq!(
&*graph1.outspends(op),
&iter::once(tx2.txid()).collect::<HashSet<_>>()
);
assert_eq!(graph2.outspends(op), graph1.outspends(op));
}
#[test]
fn insert_tx_can_retrieve_full_tx_from_graph() {
let tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::null(),
..Default::default()
}],
output: vec![TxOut::default()],
};
let mut graph = TxGraph::default();
let _ = graph.insert_tx(tx.clone());
assert_eq!(graph.get_tx(tx.txid()), Some(&tx));
}
#[test]
fn insert_tx_displaces_txouts() {
let mut tx_graph = TxGraph::default();
let tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 42_000,
script_pubkey: Script::default(),
}],
};
let _ = tx_graph.insert_txout(
OutPoint {
txid: tx.txid(),
vout: 0,
},
TxOut {
value: 1337_000,
script_pubkey: Script::default(),
},
);
let _ = tx_graph.insert_txout(
OutPoint {
txid: tx.txid(),
vout: 0,
},
TxOut {
value: 1_000_000_000,
script_pubkey: Script::default(),
},
);
let _additions = tx_graph.insert_tx(tx.clone());
assert_eq!(
tx_graph
.get_txout(OutPoint {
txid: tx.txid(),
vout: 0
})
.unwrap()
.value,
42_000
);
assert_eq!(
tx_graph.get_txout(OutPoint {
txid: tx.txid(),
vout: 1
}),
None
);
}
#[test]
fn insert_txout_does_not_displace_tx() {
let mut tx_graph = TxGraph::default();
let tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 42_000,
script_pubkey: Script::default(),
}],
};
let _additions = tx_graph.insert_tx(tx.clone());
let _ = tx_graph.insert_txout(
OutPoint {
txid: tx.txid(),
vout: 0,
},
TxOut {
value: 1337_000,
script_pubkey: Script::default(),
},
);
let _ = tx_graph.insert_txout(
OutPoint {
txid: tx.txid(),
vout: 0,
},
TxOut {
value: 1_000_000_000,
script_pubkey: Script::default(),
},
);
assert_eq!(
tx_graph
.get_txout(OutPoint {
txid: tx.txid(),
vout: 0
})
.unwrap()
.value,
42_000
);
assert_eq!(
tx_graph.get_txout(OutPoint {
txid: tx.txid(),
vout: 1
}),
None
);
}
#[test]
fn test_calculate_fee() {
let mut graph = TxGraph::default();
let intx1 = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 100,
..Default::default()
}],
};
let intx2 = Transaction {
version: 0x02,
lock_time: PackedLockTime(0),
input: vec![],
output: vec![TxOut {
value: 200,
..Default::default()
}],
};
let intxout1 = (
OutPoint {
txid: h!("dangling output"),
vout: 0,
},
TxOut {
value: 300,
..Default::default()
},
);
let _ = graph.insert_tx(intx1.clone());
let _ = graph.insert_tx(intx2.clone());
let _ = graph.insert_txout(intxout1.0, intxout1.1);
let mut tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![
TxIn {
previous_output: OutPoint {
txid: intx1.txid(),
vout: 0,
},
..Default::default()
},
TxIn {
previous_output: OutPoint {
txid: intx2.txid(),
vout: 0,
},
..Default::default()
},
TxIn {
previous_output: intxout1.0,
..Default::default()
},
],
output: vec![TxOut {
value: 500,
..Default::default()
}],
};
assert_eq!(graph.calculate_fee(&tx), Some(100));
tx.input.remove(2);
// fee would be negative
assert_eq!(graph.calculate_fee(&tx), Some(-200));
// If we have an unknown outpoint, fee should return None.
tx.input.push(TxIn {
previous_output: OutPoint {
txid: h!("unknown_txid"),
vout: 0,
},
..Default::default()
});
assert_eq!(graph.calculate_fee(&tx), None);
}
#[test]
fn test_calculate_fee_on_coinbase() {
let tx = Transaction {
version: 0x01,
lock_time: PackedLockTime(0),
input: vec![TxIn {
previous_output: OutPoint::null(),
..Default::default()
}],
output: vec![TxOut::default()],
};
let graph = TxGraph::<Transaction>::default();
assert_eq!(graph.calculate_fee(&tx), Some(0));
}
#[test]
fn test_conflicting_descendants() {
let previous_output = OutPoint::new(h!("op"), 2);
// tx_a spends previous_output
let tx_a = Transaction {
input: vec![TxIn {
previous_output,
..TxIn::default()
}],
output: vec![TxOut::default()],
..common::new_tx(0)
};
// tx_a2 spends previous_output and conflicts with tx_a
let tx_a2 = Transaction {
input: vec![TxIn {
previous_output,
..TxIn::default()
}],
output: vec![TxOut::default(), TxOut::default()],
..common::new_tx(1)
};
// tx_b spends tx_a
let tx_b = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_a.txid(), 0),
..TxIn::default()
}],
output: vec![TxOut::default()],
..common::new_tx(2)
};
let txid_a = tx_a.txid();
let txid_b = tx_b.txid();
let mut graph = TxGraph::default();
let _ = graph.insert_tx(tx_a);
let _ = graph.insert_tx(tx_b);
assert_eq!(
graph
.walk_conflicts(&tx_a2, |depth, txid| Some((depth, txid)))
.collect::<Vec<_>>(),
vec![(0_usize, txid_a), (1_usize, txid_b),],
);
}
#[test]
fn test_descendants_no_repeat() {
let tx_a = Transaction {
output: vec![TxOut::default(), TxOut::default(), TxOut::default()],
..common::new_tx(0)
};
let txs_b = (0..3)
.map(|vout| Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_a.txid(), vout),
..TxIn::default()
}],
output: vec![TxOut::default()],
..common::new_tx(1)
})
.collect::<Vec<_>>();
let txs_c = (0..2)
.map(|vout| Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(txs_b[vout as usize].txid(), vout),
..TxIn::default()
}],
output: vec![TxOut::default()],
..common::new_tx(2)
})
.collect::<Vec<_>>();
let tx_d = Transaction {
input: vec![
TxIn {
previous_output: OutPoint::new(txs_c[0].txid(), 0),
..TxIn::default()
},
TxIn {
previous_output: OutPoint::new(txs_c[1].txid(), 0),
..TxIn::default()
},
],
output: vec![TxOut::default()],
..common::new_tx(3)
};
let tx_e = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx_d.txid(), 0),
..TxIn::default()
}],
output: vec![TxOut::default()],
..common::new_tx(4)
};
let txs_not_connected = (10..20)
.map(|v| Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(h!("tx_does_not_exist"), v),
..TxIn::default()
}],
output: vec![TxOut::default()],
..common::new_tx(v)
})
.collect::<Vec<_>>();
let mut graph = TxGraph::default();
let mut expected_txids = BTreeSet::new();
// these are NOT descendants of `tx_a`
for tx in txs_not_connected {
let _ = graph.insert_tx(tx.clone());
}
// these are the expected descendants of `tx_a`
for tx in txs_b
.iter()
.chain(&txs_c)
.chain(core::iter::once(&tx_d))
.chain(core::iter::once(&tx_e))
{
let _ = graph.insert_tx(tx.clone());
assert!(expected_txids.insert(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());
}