diff --git a/crates/chain/benches/canonicalization.rs b/crates/chain/benches/canonicalization.rs index 074e38cc42..7fcec92c3e 100644 --- a/crates/chain/benches/canonicalization.rs +++ b/crates/chain/benches/canonicalization.rs @@ -3,10 +3,8 @@ use bdk_chain::{keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, Ind use bdk_core::{BlockId, CheckPoint}; use bdk_core::{ConfirmationBlockTime, TxUpdate}; use bdk_testenv::hash; -use bitcoin::{ - absolute, constants, hashes::Hash, key::Secp256k1, transaction, Amount, BlockHash, Network, - OutPoint, ScriptBuf, Transaction, TxIn, TxOut, -}; +use bdk_testenv::utils::{genesis_block_id, new_standard_tx, spk_at_index, tip_block_id}; +use bitcoin::{key::Secp256k1, Amount, OutPoint, Transaction, TxIn, TxOut}; use criterion::{criterion_group, criterion_main, Criterion}; use miniscript::{Descriptor, DescriptorPublicKey}; use std::sync::Arc; @@ -14,43 +12,11 @@ use std::sync::Arc; type Keychain = (); type KeychainTxGraph = IndexedTxGraph>; -/// New tx guaranteed to have at least one output -fn new_tx(lt: u32) -> Transaction { - Transaction { - version: transaction::Version::TWO, - lock_time: absolute::LockTime::from_consensus(lt), - input: vec![], - output: vec![TxOut::NULL], - } -} - -fn spk_at_index(txout_index: &KeychainTxOutIndex, index: u32) -> ScriptBuf { - txout_index - .get_descriptor(()) - .unwrap() - .at_derivation_index(index) - .unwrap() - .script_pubkey() -} - -fn genesis_block_id() -> BlockId { - BlockId { - height: 0, - hash: constants::genesis_block(Network::Regtest).block_hash(), - } -} - -fn tip_block_id() -> BlockId { - BlockId { - height: 100, - hash: BlockHash::all_zeros(), - } -} - /// Add ancestor tx confirmed at `block_id` with `locktime` (used for uniqueness). /// The transaction always pays 1 BTC to SPK 0. fn add_ancestor_tx(graph: &mut KeychainTxGraph, block_id: BlockId, locktime: u32) -> OutPoint { - let spk_0 = spk_at_index(&graph.index, 0); + let descriptor = graph.index.get_descriptor(()).unwrap(); + let spk_0 = spk_at_index(descriptor, 0); let tx = Transaction { input: vec![TxIn { previous_output: OutPoint::new(hash!("bogus"), locktime), @@ -60,7 +26,7 @@ fn add_ancestor_tx(graph: &mut KeychainTxGraph, block_id: BlockId, locktime: u32 value: Amount::ONE_BTC, script_pubkey: spk_0, }], - ..new_tx(locktime) + ..new_standard_tx(locktime) }; let txid = tx.compute_txid(); let _ = graph.insert_tx(tx); @@ -77,7 +43,7 @@ fn add_ancestor_tx(graph: &mut KeychainTxGraph, block_id: BlockId, locktime: u32 fn setup(f: F) -> (KeychainTxGraph, LocalChain) { const DESC: &str = "tr([ab28dc00/86h/1h/0h]tpubDCdDtzAMZZrkwKBxwNcGCqe4FRydeD9rfMisoi7qLdraG79YohRfPW4YgdKQhpgASdvh612xXNY5xYzoqnyCgPbkpK4LSVcH5Xv4cK7johH/0/*)"; let cp = CheckPoint::from_blocks( - [genesis_block_id(), tip_block_id()] + [genesis_block_id(), tip_block_id(100)] .into_iter() .map(|block_id| (block_id.height, block_id.hash)), ) @@ -127,9 +93,10 @@ fn run_filter_chain_unspents(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp pub fn many_conflicting_unconfirmed(c: &mut Criterion) { const CONFLICTING_TX_COUNT: u32 = 2100; let (tx_graph, chain) = std::hint::black_box(setup(|tx_graph, _chain| { - let previous_output = add_ancestor_tx(tx_graph, tip_block_id(), 0); + let previous_output = add_ancestor_tx(tx_graph, tip_block_id(100), 0); + let descriptor = tx_graph.index.get_descriptor(()).unwrap(); // Create conflicting txs that spend from `previous_output`. - let spk_1 = spk_at_index(&tx_graph.index, 1); + let spk_1 = spk_at_index(descriptor, 1); for i in 1..=CONFLICTING_TX_COUNT { let tx = Transaction { input: vec![TxIn { @@ -140,7 +107,7 @@ pub fn many_conflicting_unconfirmed(c: &mut Criterion) { value: Amount::ONE_BTC - Amount::from_sat(i as u64 * 10), script_pubkey: spk_1.clone(), }], - ..new_tx(i) + ..new_standard_tx(i) }; let mut update = TxUpdate::default(); update.seen_ats = [(tx.compute_txid(), i as u64)].into(); @@ -165,7 +132,7 @@ pub fn many_conflicting_unconfirmed(c: &mut Criterion) { pub fn many_chained_unconfirmed(c: &mut Criterion) { const TX_CHAIN_COUNT: u32 = 2100; let (tx_graph, chain) = std::hint::black_box(setup(|tx_graph, _chain| { - let mut previous_output = add_ancestor_tx(tx_graph, tip_block_id(), 0); + let mut previous_output = add_ancestor_tx(tx_graph, tip_block_id(100), 0); // Create a chain of unconfirmed txs where each subsequent tx spends the output of the // previous one. for i in 0..TX_CHAIN_COUNT { @@ -175,7 +142,7 @@ pub fn many_chained_unconfirmed(c: &mut Criterion) { previous_output, ..Default::default() }], - ..new_tx(i) + ..new_standard_tx(i) }; let txid = tx.compute_txid(); let mut update = TxUpdate::default(); @@ -204,7 +171,7 @@ pub fn nested_conflicts(c: &mut Criterion) { const CONFLICTS_PER_OUTPUT: usize = 3; const GRAPH_DEPTH: usize = 7; let (tx_graph, chain) = std::hint::black_box(setup(|tx_graph, _chain| { - let mut prev_ops = core::iter::once(add_ancestor_tx(tx_graph, tip_block_id(), 0)) + let mut prev_ops = core::iter::once(add_ancestor_tx(tx_graph, tip_block_id(100), 0)) .collect::>(); for depth in 1..GRAPH_DEPTH { for previous_output in core::mem::take(&mut prev_ops) { @@ -225,7 +192,7 @@ pub fn nested_conflicts(c: &mut Criterion) { value, script_pubkey, }], - ..new_tx(conflict_i as _) + ..new_standard_tx(conflict_i as _) }; let txid = tx.compute_txid(); prev_ops.push(OutPoint::new(txid, 0)); diff --git a/crates/chain/benches/indexer.rs b/crates/chain/benches/indexer.rs index 5907c76a00..d1175684f9 100644 --- a/crates/chain/benches/indexer.rs +++ b/crates/chain/benches/indexer.rs @@ -3,11 +3,9 @@ use bdk_chain::{ local_chain::LocalChain, CanonicalizationParams, IndexedTxGraph, }; -use bdk_core::{BlockId, CheckPoint, ConfirmationBlockTime, TxUpdate}; -use bitcoin::{ - absolute, constants, hashes::Hash, key::Secp256k1, transaction, Amount, BlockHash, Network, - Transaction, TxIn, TxOut, -}; +use bdk_core::{CheckPoint, ConfirmationBlockTime, TxUpdate}; +use bdk_testenv::utils::{genesis_block_id, new_standard_tx, tip_block_id}; +use bitcoin::{key::Secp256k1, Amount, Transaction, TxIn, TxOut}; use criterion::{criterion_group, criterion_main, Criterion}; use miniscript::Descriptor; use std::sync::Arc; @@ -22,36 +20,13 @@ const TX_CT: u32 = 21; const USE_SPK_CACHE: bool = true; const AMOUNT: Amount = Amount::from_sat(1_000); -fn new_tx(lt: u32) -> Transaction { - Transaction { - version: transaction::Version::TWO, - lock_time: absolute::LockTime::from_consensus(lt), - input: vec![], - output: vec![TxOut::NULL], - } -} - -fn genesis_block_id() -> BlockId { - BlockId { - height: 0, - hash: constants::genesis_block(Network::Regtest).block_hash(), - } -} - -fn tip_block_id() -> BlockId { - BlockId { - height: 100, - hash: BlockHash::all_zeros(), - } -} - fn setup(f: F) -> (KeychainTxGraph, LocalChain) { let desc = Descriptor::parse_descriptor(&Secp256k1::new(), DESC) .unwrap() .0; let cp = CheckPoint::from_blocks( - [genesis_block_id(), tip_block_id()] + [genesis_block_id(), tip_block_id(100)] .into_iter() .map(|block_id| (block_id.height, block_id.hash)), ) @@ -101,7 +76,7 @@ pub fn reindex_tx_graph(c: &mut Criterion) { script_pubkey, value: AMOUNT, }], - ..new_tx(i) + ..new_standard_tx(i) }; let txid = tx.compute_txid(); let mut update = TxUpdate::default(); diff --git a/crates/chain/tests/common/mod.rs b/crates/chain/tests/common/mod.rs deleted file mode 100644 index cb3ee66f37..0000000000 --- a/crates/chain/tests/common/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -#![cfg(feature = "miniscript")] - -mod tx_template; -#[allow(unused_imports)] -pub use tx_template::*; diff --git a/crates/chain/tests/common/tx_template.rs b/crates/chain/tests/common/tx_template.rs deleted file mode 100644 index 29f36169ad..0000000000 --- a/crates/chain/tests/common/tx_template.rs +++ /dev/null @@ -1,159 +0,0 @@ -#![cfg(feature = "miniscript")] - -use bdk_testenv::utils::DESCRIPTORS; -use rand::distributions::{Alphanumeric, DistString}; -use std::collections::HashMap; - -use bdk_chain::{spk_txout::SpkTxOutIndex, tx_graph::TxGraph, Anchor, CanonicalizationParams}; -use bitcoin::{ - locktime::absolute::LockTime, secp256k1::Secp256k1, transaction, Amount, OutPoint, ScriptBuf, - Sequence, Transaction, TxIn, TxOut, Txid, Witness, -}; -use miniscript::Descriptor; - -/// Template for creating a transaction in `TxGraph`. -/// -/// The incentive for transaction templates is to create a transaction history in a simple manner to -/// avoid having to explicitly hash previous transactions to form previous outpoints of later -/// transactions. -#[derive(Clone, Copy, Default)] -pub struct TxTemplate<'a, A> { - /// Uniquely identifies the transaction, before it can have a txid. - pub tx_name: &'a str, - pub inputs: &'a [TxInTemplate<'a>], - pub outputs: &'a [TxOutTemplate], - pub anchors: &'a [A], - pub last_seen: Option, - pub assume_canonical: bool, -} - -#[allow(dead_code)] -pub enum TxInTemplate<'a> { - /// This will give a random txid and vout. - Bogus, - - /// This is used for coinbase transactions because they do not have previous outputs. - Coinbase, - - /// Contains the `tx_name` and `vout` that we are spending. The rule is that we must only spend - /// from tx of a previous `TxTemplate`. - PrevTx(&'a str, usize), -} - -pub struct TxOutTemplate { - pub value: u64, - pub spk_index: Option, // some = get spk from SpkTxOutIndex, none = random spk -} - -#[allow(unused)] -impl TxOutTemplate { - pub fn new(value: u64, spk_index: Option) -> Self { - TxOutTemplate { value, spk_index } - } -} - -#[allow(dead_code)] -pub struct TxTemplateEnv<'a, A> { - pub tx_graph: TxGraph, - pub indexer: SpkTxOutIndex, - pub txid_to_name: HashMap<&'a str, Txid>, - pub canonicalization_params: CanonicalizationParams, -} - -#[allow(dead_code)] -pub fn init_graph<'a, A: Anchor + Clone + 'a>( - tx_templates: impl IntoIterator>, -) -> TxTemplateEnv<'a, A> { - let (descriptor, _) = - Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[2]).unwrap(); - let mut tx_graph = TxGraph::::default(); - let mut indexer = SpkTxOutIndex::default(); - (0..10).for_each(|index| { - indexer.insert_spk( - index, - descriptor - .at_derivation_index(index) - .unwrap() - .script_pubkey(), - ); - }); - let mut txid_to_name = HashMap::<&'a str, Txid>::new(); - - let mut canonicalization_params = CanonicalizationParams::default(); - for (bogus_txin_vout, tx_tmp) in tx_templates.into_iter().enumerate() { - let tx = Transaction { - version: transaction::Version::non_standard(0), - lock_time: LockTime::ZERO, - input: tx_tmp - .inputs - .iter() - .map(|input| match input { - TxInTemplate::Bogus => TxIn { - previous_output: OutPoint::new( - bitcoin::hashes::Hash::hash( - Alphanumeric - .sample_string(&mut rand::thread_rng(), 20) - .as_bytes(), - ), - bogus_txin_vout as u32, - ), - script_sig: ScriptBuf::new(), - sequence: Sequence::default(), - witness: Witness::new(), - }, - TxInTemplate::Coinbase => TxIn { - previous_output: OutPoint::null(), - script_sig: ScriptBuf::new(), - sequence: Sequence::MAX, - witness: Witness::new(), - }, - TxInTemplate::PrevTx(prev_name, prev_vout) => { - let prev_txid = txid_to_name.get(prev_name).expect( - "txin template must spend from tx of template that comes before", - ); - TxIn { - previous_output: OutPoint::new(*prev_txid, *prev_vout as _), - script_sig: ScriptBuf::new(), - sequence: Sequence::default(), - witness: Witness::new(), - } - } - }) - .collect(), - output: tx_tmp - .outputs - .iter() - .map(|output| match &output.spk_index { - None => TxOut { - value: Amount::from_sat(output.value), - script_pubkey: ScriptBuf::new(), - }, - Some(index) => TxOut { - value: Amount::from_sat(output.value), - script_pubkey: indexer.spk_at_index(index).unwrap(), - }, - }) - .collect(), - }; - - let txid = tx.compute_txid(); - if tx_tmp.assume_canonical { - canonicalization_params.assume_canonical.push(txid); - } - txid_to_name.insert(tx_tmp.tx_name, txid); - indexer.scan(&tx); - let _ = tx_graph.insert_tx(tx.clone()); - for anchor in tx_tmp.anchors.iter() { - let _ = tx_graph.insert_anchor(txid, anchor.clone()); - } - if let Some(last_seen) = tx_tmp.last_seen { - let _ = tx_graph.insert_seen_at(txid, last_seen); - } - } - TxTemplateEnv { - tx_graph, - indexer, - txid_to_name, - canonicalization_params, - } -} diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 18d1ff1bf4..0a62f821a5 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -1,8 +1,5 @@ #![cfg(feature = "miniscript")] -#[macro_use] -mod common; - use std::{collections::BTreeSet, str::FromStr, sync::Arc}; use bdk_chain::{ @@ -17,7 +14,7 @@ use bdk_testenv::{ anyhow::{self}, block_id, corepc_node::{Input, Output}, - hash, + hash, spk, utils::{new_tx, DESCRIPTORS}, TestEnv, }; @@ -27,16 +24,6 @@ use bitcoin::{ }; use miniscript::Descriptor; -fn gen_spk() -> ScriptBuf { - use bitcoin::secp256k1::{Secp256k1, SecretKey}; - - let secp = Secp256k1::new(); - let (x_only_pk, _) = SecretKey::new(&mut rand::thread_rng()) - .public_key(&secp) - .x_only_public_key(); - ScriptBuf::new_p2tr(&secp, x_only_pk, None) -} - /// Conflicts of relevant transactions must also be considered relevant. /// /// This allows the receiving structures to determine the reason why a given transaction is not part @@ -68,7 +55,7 @@ fn relevant_conflicts() -> anyhow::Result<()> { .address()? .require_network(Network::Regtest)?; - let recv_spk = gen_spk(); + let recv_spk = spk!(); let recv_addr = Address::from_script(&recv_spk, &bitcoin::params::REGTEST)?; let mut graph = SpkTxGraph::default(); diff --git a/crates/chain/tests/test_keychain_txout_index.rs b/crates/chain/tests/test_keychain_txout_index.rs index 263a0fa86f..125b472482 100644 --- a/crates/chain/tests/test_keychain_txout_index.rs +++ b/crates/chain/tests/test_keychain_txout_index.rs @@ -7,9 +7,9 @@ use bdk_chain::{ }; use bdk_testenv::{ hash, - utils::{new_tx, DESCRIPTORS}, + utils::{new_tx, spk_at_index, DESCRIPTORS}, }; -use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxOut}; +use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, TxOut}; use miniscript::{Descriptor, DescriptorPublicKey}; #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)] @@ -52,13 +52,6 @@ fn init_txout_index( txout_index } -fn spk_at_index(descriptor: &Descriptor, index: u32) -> ScriptBuf { - descriptor - .derived_descriptor(&Secp256k1::verification_only(), index) - .expect("must derive") - .script_pubkey() -} - // We create two empty changesets lhs and rhs, we then insert various descriptors with various // last_revealed, merge rhs to lhs, and check that the result is consistent with these rules: // - Existing index doesn't update if the new index in `other` is lower than `self`. diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index b2a3596085..fbb52f57fb 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1,7 +1,5 @@ #![cfg(feature = "miniscript")] -#[macro_use] -mod common; use bdk_chain::{collections::*, BlockId, CanonicalizationParams, ConfirmationBlockTime}; use bdk_chain::{ local_chain::LocalChain, @@ -10,13 +8,13 @@ use bdk_chain::{ Anchor, ChainOracle, ChainPosition, Merge, }; use bdk_testenv::{block_id, hash, utils::new_tx}; +use bdk_testenv::{init_graph, TxInTemplate, TxOutTemplate, TxTemplate}; use bitcoin::hex::FromHex; use bitcoin::Witness; use bitcoin::{ absolute, hashes::Hash, transaction, Amount, BlockHash, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut, Txid, }; -use common::*; use core::iter; use rand::RngCore; use std::sync::Arc; @@ -1097,9 +1095,7 @@ fn test_chain_spends() { // Because this tx conflicts with an already confirmed transaction, chain position should // return none. - assert!(canonical_positions - .get(&tx_1_conflict.compute_txid()) - .is_none()); + assert!(!canonical_positions.contains_key(&tx_1_conflict.compute_txid())); } // Another conflicting tx that conflicts with tx_2. @@ -1143,7 +1139,7 @@ fn test_chain_spends() { ); // Chain position of the `tx_2` is now none, as it is older than `tx_2_conflict` - assert!(canonical_positions.get(&tx_2.compute_txid()).is_none()); + assert!(!canonical_positions.contains_key(&tx_2.compute_txid())); } } @@ -1282,30 +1278,20 @@ fn call_map_anchors_with_non_deterministic_anchor() { } let template = [ - TxTemplate { - tx_name: "tx1", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10000, Some(1))], - anchors: &[block_id!(1, "A")], - last_seen: None, - ..Default::default() - }, - TxTemplate { - tx_name: "tx2", - inputs: &[TxInTemplate::PrevTx("tx1", 0)], - outputs: &[TxOutTemplate::new(20000, Some(2))], - anchors: &[block_id!(2, "B")], - ..Default::default() - }, - TxTemplate { - tx_name: "tx3", - inputs: &[TxInTemplate::PrevTx("tx2", 0)], - outputs: &[TxOutTemplate::new(30000, Some(3))], - anchors: &[block_id!(3, "C"), block_id!(4, "D")], - ..Default::default() - }, + TxTemplate::new("tx1") + .with_inputs(vec![TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(1))]) + .with_anchors(vec![block_id!(1, "A")]), + TxTemplate::new("tx2") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(2))]) + .with_anchors(vec![block_id!(2, "B")]), + TxTemplate::new("tx3") + .with_inputs(vec![TxInTemplate::PrevTx("tx2".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(30_000, Some(3))]) + .with_anchors(vec![block_id!(3, "C"), block_id!(4, "D")]), ]; - let graph = init_graph(&template).tx_graph; + let graph = init_graph(template).tx_graph; let new_graph = graph.clone().map_anchors(|a| NonDeterministicAnchor { anchor_block: a, // A non-deterministic value diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index 38f21365c3..0d073918b2 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -1,26 +1,23 @@ #![cfg(feature = "miniscript")] -#[macro_use] -mod common; - use bdk_chain::{local_chain::LocalChain, Balance, BlockId}; use bdk_testenv::{block_id, hash, local_chain}; +use bdk_testenv::{init_graph, TxInTemplate, TxOutTemplate, TxTemplate}; use bitcoin::{Amount, BlockHash, OutPoint}; -use common::*; use std::collections::{BTreeSet, HashSet}; #[allow(dead_code)] -struct Scenario<'a> { +struct Scenario { /// Name of the test scenario - name: &'a str, + name: &'static str, /// Transaction templates - tx_templates: &'a [TxTemplate<'a, BlockId>], + tx_templates: Vec>, /// Names of txs that must exist in the output of `list_canonical_txs` - exp_chain_txs: HashSet<&'a str>, + exp_chain_txs: HashSet<&'static str>, /// Outpoints that must exist in the output of `filter_chain_txouts` - exp_chain_txouts: HashSet<(&'a str, u32)>, + exp_chain_txouts: HashSet<(&'static str, u32)>, /// Outpoints of UTXOs that must exist in the output of `filter_chain_unspents` - exp_unspents: HashSet<(&'a str, u32)>, + exp_unspents: HashSet<(&'static str, u32)>, /// Expected balances exp_balance: Balance, } @@ -46,37 +43,22 @@ fn test_tx_conflict_handling() { let scenarios = [ Scenario { name: "coinbase tx cannot be in mempool and be unconfirmed", - tx_templates: &[ - TxTemplate { - tx_name: "unconfirmed_coinbase", - inputs: &[TxInTemplate::Coinbase], - outputs: &[TxOutTemplate::new(5000, Some(0))], - ..Default::default() - }, - TxTemplate { - tx_name: "confirmed_genesis", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10000, Some(1))], - anchors: &[block_id!(1, "B")], - last_seen: None, - ..Default::default() - }, - TxTemplate { - tx_name: "unconfirmed_conflict", - inputs: &[ - TxInTemplate::PrevTx("confirmed_genesis", 0), - TxInTemplate::PrevTx("unconfirmed_coinbase", 0) - ], - outputs: &[TxOutTemplate::new(20000, Some(2))], - ..Default::default() - }, - TxTemplate { - tx_name: "confirmed_conflict", - inputs: &[TxInTemplate::PrevTx("confirmed_genesis", 0)], - outputs: &[TxOutTemplate::new(20000, Some(3))], - anchors: &[block_id!(4, "E")], - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("unconfirmed_coinbase") + .with_inputs(vec![TxInTemplate::Coinbase]) + .with_outputs(vec![TxOutTemplate::new(5_000, Some(0))]), + TxTemplate::new("confirmed_genesis") + .with_inputs(vec![TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(1))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("unconfirmed_conflict") + .with_inputs(vec![TxInTemplate::PrevTx("confirmed_genesis".into(), 0), + TxInTemplate::PrevTx("unconfirmed_coinbase".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(2))]), + TxTemplate::new("confirmed_conflict") + .with_inputs(vec![TxInTemplate::PrevTx("confirmed_genesis".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(3))]) + .with_anchors(vec![block_id!(4, "E")]), ], exp_chain_txs: HashSet::from(["confirmed_genesis", "confirmed_conflict"]), exp_chain_txouts: HashSet::from([("confirmed_genesis", 0), ("confirmed_conflict", 0)]), @@ -88,28 +70,18 @@ fn test_tx_conflict_handling() { }, Scenario { name: "2 unconfirmed txs with same last_seens conflict", - tx_templates: &[ - TxTemplate { - tx_name: "tx1", - outputs: &[TxOutTemplate::new(40000, Some(0))], - anchors: &[block_id!(1, "B")], - last_seen: None, - ..Default::default() - }, - TxTemplate { - tx_name: "tx_conflict_1", - inputs: &[TxInTemplate::PrevTx("tx1", 0)], - outputs: &[TxOutTemplate::new(20000, Some(2))], - last_seen: Some(300), - ..Default::default() - }, - TxTemplate { - tx_name: "tx_conflict_2", - inputs: &[TxInTemplate::PrevTx("tx1", 0)], - outputs: &[TxOutTemplate::new(30000, Some(3))], - last_seen: Some(300), - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("tx1") + .with_outputs(vec![TxOutTemplate::new(40_000, Some(0))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("tx_conflict_1") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(2))]) + .with_last_seen(Some(300)), + TxTemplate::new("tx_conflict_2") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(30000, Some(3))]) + .with_last_seen(Some(300)), ], // the txgraph is going to pick tx_conflict_2 because of higher lexicographical txid exp_chain_txs: HashSet::from(["tx1", "tx_conflict_2"]), @@ -124,29 +96,19 @@ fn test_tx_conflict_handling() { }, Scenario { name: "2 unconfirmed txs with different last_seens conflict", - tx_templates: &[ - TxTemplate { - tx_name: "tx1", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))], - anchors: &[block_id!(1, "B")], - last_seen: None, - ..Default::default() - }, - TxTemplate { - tx_name: "tx_conflict_1", - inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(20000, Some(2))], - last_seen: Some(200), - ..Default::default() - }, - TxTemplate { - tx_name: "tx_conflict_2", - inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::PrevTx("tx1", 1)], - outputs: &[TxOutTemplate::new(30000, Some(3))], - last_seen: Some(300), - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("tx1") + .with_inputs(vec![TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(0)), TxOutTemplate::new(10_000, Some(1))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("tx_conflict_1") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0), TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(2))]) + .with_last_seen(Some(200)), + TxTemplate::new("tx_conflict_2") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0), TxInTemplate::PrevTx("tx1".into(), 1)]) + .with_outputs(vec![TxOutTemplate::new(30_000, Some(3))]) + .with_last_seen(Some(300)) ], exp_chain_txs: HashSet::from(["tx1", "tx_conflict_2"]), exp_chain_txouts: HashSet::from([("tx1", 0), ("tx1", 1), ("tx_conflict_2", 0)]), @@ -160,36 +122,23 @@ fn test_tx_conflict_handling() { }, Scenario { name: "3 unconfirmed txs with different last_seens conflict", - tx_templates: &[ - TxTemplate { - tx_name: "tx1", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], - last_seen: None, - ..Default::default() - }, - TxTemplate { - tx_name: "tx_conflict_1", - inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(20000, Some(1))], - last_seen: Some(200), - ..Default::default() - }, - TxTemplate { - tx_name: "tx_conflict_2", - inputs: &[TxInTemplate::PrevTx("tx1", 0)], - outputs: &[TxOutTemplate::new(30000, Some(2))], - last_seen: Some(300), - ..Default::default() - }, - TxTemplate { - tx_name: "tx_conflict_3", - inputs: &[TxInTemplate::PrevTx("tx1", 0)], - outputs: &[TxOutTemplate::new(40000, Some(3))], - last_seen: Some(400), - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("tx1") + .with_inputs(vec![TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(10000, Some(0))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("tx_conflict_1") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0), TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(20000, Some(1))]) + .with_last_seen(Some(200)), + TxTemplate::new("tx_conflict_2") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(30000, Some(2))]) + .with_last_seen(Some(300)), + TxTemplate::new("tx_conflict_3") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(40000, Some(3))]) + .with_last_seen(Some(400)), ], exp_chain_txs: HashSet::from(["tx1", "tx_conflict_3"]), exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_3", 0)]), @@ -203,30 +152,20 @@ fn test_tx_conflict_handling() { }, Scenario { name: "unconfirmed tx conflicts with tx in orphaned block, orphaned higher last_seen", - tx_templates: &[ - TxTemplate { - tx_name: "tx1", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], - last_seen: None, - ..Default::default() - }, - TxTemplate { - tx_name: "tx_conflict_1", - inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(20000, Some(1))], - last_seen: Some(200), - ..Default::default() - }, - TxTemplate { - tx_name: "tx_orphaned_conflict", - inputs: &[TxInTemplate::PrevTx("tx1", 0)], - outputs: &[TxOutTemplate::new(30000, Some(2))], - anchors: &[block_id!(4, "Orphaned Block")], - last_seen: Some(300), - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("tx1") + .with_inputs(vec![TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(0))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("tx_conflict_1") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0), TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(1))]) + .with_last_seen(Some(200)), + TxTemplate::new("tx_orphaned_conflict") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(30_000, Some(2))]) + .with_anchors(vec![block_id!(4, "Orphaned Block")]) + .with_last_seen(Some(300)), ], exp_chain_txs: HashSet::from(["tx1", "tx_orphaned_conflict"]), exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_orphaned_conflict", 0)]), @@ -240,30 +179,20 @@ fn test_tx_conflict_handling() { }, Scenario { name: "unconfirmed tx conflicts with tx in orphaned block, orphaned lower last_seen", - tx_templates: &[ - TxTemplate { - tx_name: "tx1", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], - last_seen: None, - ..Default::default() - }, - TxTemplate { - tx_name: "tx_conflict_1", - inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(20000, Some(1))], - last_seen: Some(200), - ..Default::default() - }, - TxTemplate { - tx_name: "tx_orphaned_conflict", - inputs: &[TxInTemplate::PrevTx("tx1", 0)], - outputs: &[TxOutTemplate::new(30000, Some(2))], - anchors: &[block_id!(4, "Orphaned Block")], - last_seen: Some(100), - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("tx1") + .with_inputs(vec![TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(0))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("tx_conflict_1") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0), TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(1))]) + .with_last_seen(Some(200)), + TxTemplate::new("tx_orphaned_conflict") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(30_000, Some(2))]) + .with_anchors(vec![block_id!(4, "Orphaned Block")]) + .with_last_seen(Some(100)), ], exp_chain_txs: HashSet::from(["tx1", "tx_conflict_1"]), exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_1", 0)]), @@ -277,43 +206,27 @@ fn test_tx_conflict_handling() { }, Scenario { name: "multiple unconfirmed txs conflict with a confirmed tx", - tx_templates: &[ - TxTemplate { - tx_name: "tx1", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], - last_seen: None, - ..Default::default() - }, - TxTemplate { - tx_name: "tx_conflict_1", - inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(20000, Some(1))], - last_seen: Some(200), - ..Default::default() - }, - TxTemplate { - tx_name: "tx_conflict_2", - inputs: &[TxInTemplate::PrevTx("tx1", 0)], - outputs: &[TxOutTemplate::new(30000, Some(2))], - last_seen: Some(300), - ..Default::default() - }, - TxTemplate { - tx_name: "tx_conflict_3", - inputs: &[TxInTemplate::PrevTx("tx1", 0)], - outputs: &[TxOutTemplate::new(40000, Some(3))], - last_seen: Some(400), - ..Default::default() - }, - TxTemplate { - tx_name: "tx_confirmed_conflict", - inputs: &[TxInTemplate::PrevTx("tx1", 0)], - outputs: &[TxOutTemplate::new(50000, Some(4))], - anchors: &[block_id!(1, "B")], - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("tx1") + .with_inputs(vec![TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(0))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("tx_conflict_1") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0), TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(1))]) + .with_last_seen(Some(200)), + TxTemplate::new("tx_conflict_2") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(30_000, Some(2))]) + .with_last_seen(Some(300)), + TxTemplate::new("tx_conflict_3") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(40_000, Some(3))]) + .with_last_seen(Some(400)), + TxTemplate::new("tx_confirmed_conflict") + .with_inputs(vec![TxInTemplate::PrevTx("tx1".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(50_000, Some(4))]) + .with_anchors(vec![block_id!(1, "B")]), ], exp_chain_txs: HashSet::from(["tx1", "tx_confirmed_conflict"]), exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_confirmed_conflict", 0)]), @@ -327,35 +240,23 @@ fn test_tx_conflict_handling() { }, Scenario { name: "B and B' spend A and conflict, C spends B, all the transactions are unconfirmed, B' has higher last_seen than B", - tx_templates: &[ - TxTemplate { - tx_name: "A", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10000, Some(0))], - last_seen: Some(22), - ..Default::default() - }, - TxTemplate { - tx_name: "B", - inputs: &[TxInTemplate::PrevTx("A", 0)], - outputs: &[TxOutTemplate::new(20000, Some(1))], - last_seen: Some(23), - ..Default::default() - }, - TxTemplate { - tx_name: "B'", - inputs: &[TxInTemplate::PrevTx("A", 0)], - outputs: &[TxOutTemplate::new(20000, Some(2))], - last_seen: Some(24), - ..Default::default() - }, - TxTemplate { - tx_name: "C", - inputs: &[TxInTemplate::PrevTx("B", 0)], - outputs: &[TxOutTemplate::new(30000, Some(3))], - last_seen: Some(25), - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("A") + .with_inputs(vec![TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(0))]) + .with_last_seen(Some(22)), + TxTemplate::new("B") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(1))]) + .with_last_seen(Some(23)), + TxTemplate::new("B'") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(2))]) + .with_last_seen(Some(24)), + TxTemplate::new("C") + .with_inputs(vec![TxInTemplate::PrevTx("B".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(30_000, Some(3))]) + .with_last_seen(Some(25)) ], // A, B, C will appear in the list methods // This is because B' has a higher last seen than B, but C has a higher @@ -372,34 +273,21 @@ fn test_tx_conflict_handling() { }, Scenario { name: "B and B' spend A and conflict, C spends B, A and B' are in best chain", - tx_templates: &[ - TxTemplate { - tx_name: "A", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], - last_seen: None, - ..Default::default() - }, - TxTemplate { - tx_name: "B", - inputs: &[TxInTemplate::PrevTx("A", 0)], - outputs: &[TxOutTemplate::new(20000, Some(1))], - ..Default::default() - }, - TxTemplate { - tx_name: "B'", - inputs: &[TxInTemplate::PrevTx("A", 0)], - outputs: &[TxOutTemplate::new(20000, Some(2))], - anchors: &[block_id!(4, "E")], - ..Default::default() - }, - TxTemplate { - tx_name: "C", - inputs: &[TxInTemplate::PrevTx("B", 0)], - outputs: &[TxOutTemplate::new(30000, Some(3))], - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("A") + .with_inputs(&[TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(0))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("B") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(1))]), + TxTemplate::new("B'") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20000, Some(2))]) + .with_anchors(vec![block_id!(4, "E")]), + TxTemplate::new("C") + .with_inputs(vec![TxInTemplate::PrevTx("B".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(30_000, Some(3))]), ], // B and C should not appear in the list methods exp_chain_txs: HashSet::from(["A", "B'"]), @@ -414,35 +302,23 @@ fn test_tx_conflict_handling() { }, Scenario { name: "B and B' spend A and conflict, C spends B', A and B' are in best chain", - tx_templates: &[ - TxTemplate { - tx_name: "A", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], - ..Default::default() - }, - TxTemplate { - tx_name: "B", - inputs: &[TxInTemplate::PrevTx("A", 0)], - outputs: &[TxOutTemplate::new(20000, Some(1))], - last_seen: Some(2), - ..Default::default() - }, - TxTemplate { - tx_name: "B'", - inputs: &[TxInTemplate::PrevTx("A", 0)], - outputs: &[TxOutTemplate::new(20000, Some(2))], - anchors: &[block_id!(4, "E")], - ..Default::default() - }, - TxTemplate { - tx_name: "C", - inputs: &[TxInTemplate::PrevTx("B'", 0)], - outputs: &[TxOutTemplate::new(30000, Some(3))], - last_seen: Some(1), - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("A") + .with_inputs(vec![TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(0))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("B") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(1))]) + .with_last_seen(Some(2)), + TxTemplate::new("B'") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(2))]) + .with_anchors(vec![block_id!(4, "E")]), + TxTemplate::new("C") + .with_inputs(vec![TxInTemplate::PrevTx("B'".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(30_000, Some(3))]) + .with_last_seen(Some(1)) ], // B should not appear in the list methods exp_chain_txs: HashSet::from(["A", "B'", "C"]), @@ -461,38 +337,22 @@ fn test_tx_conflict_handling() { }, Scenario { name: "B and B' spend A and conflict, C spends both B and B', A is in best chain", - tx_templates: &[ - TxTemplate { - tx_name: "A", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], - last_seen: None, - ..Default::default() - }, - TxTemplate { - tx_name: "B", - inputs: &[TxInTemplate::PrevTx("A", 0), TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(20000, Some(1))], - last_seen: Some(200), - ..Default::default() - }, - TxTemplate { - tx_name: "B'", - inputs: &[TxInTemplate::PrevTx("A", 0)], - outputs: &[TxOutTemplate::new(30000, Some(2))], - last_seen: Some(300), - ..Default::default() - }, - TxTemplate { - tx_name: "C", - inputs: &[ - TxInTemplate::PrevTx("B", 0), - TxInTemplate::PrevTx("B'", 0), - ], - outputs: &[TxOutTemplate::new(20000, Some(3))], - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("A") + .with_inputs(vec![TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(0))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("B") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0), TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(1))]) + .with_last_seen(Some(200)), + TxTemplate::new("B'") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(30_000, Some(2))]) + .with_last_seen(Some(300)), + TxTemplate::new("C") + .with_inputs(vec![TxInTemplate::PrevTx("B".into(), 0), TxInTemplate::PrevTx("B'".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(3))]), ], // C should not appear in the list methods exp_chain_txs: HashSet::from(["A", "B'"]), @@ -507,38 +367,22 @@ fn test_tx_conflict_handling() { }, Scenario { name: "B and B' spend A and conflict, B' is confirmed, C spends both B and B', A is in best chain", - tx_templates: &[ - TxTemplate { - tx_name: "A", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], - last_seen: None, - ..Default::default() - }, - TxTemplate { - tx_name: "B", - inputs: &[TxInTemplate::PrevTx("A", 0), TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(20000, Some(1))], - last_seen: Some(200), - ..Default::default() - }, - TxTemplate { - tx_name: "B'", - inputs: &[TxInTemplate::PrevTx("A", 0)], - outputs: &[TxOutTemplate::new(50000, Some(4))], - anchors: &[block_id!(1, "B")], - ..Default::default() - }, - TxTemplate { - tx_name: "C", - inputs: &[ - TxInTemplate::PrevTx("B", 0), - TxInTemplate::PrevTx("B'", 0), - ], - outputs: &[TxOutTemplate::new(20000, Some(5))], - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("A") + .with_inputs(&[TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(0))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("B") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0), TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(1))]) + .with_last_seen(Some(200)), + TxTemplate::new("B'") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(50_000, Some(4))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("C") + .with_inputs(vec![TxInTemplate::PrevTx("B".into(), 0), TxInTemplate::PrevTx("B'".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(5))]), ], // C should not appear in the list methods exp_chain_txs: HashSet::from(["A", "B'"]), @@ -553,44 +397,25 @@ fn test_tx_conflict_handling() { }, Scenario { name: "B and B' spend A and conflict, B' is confirmed, C spends both B and B', D spends C, A is in best chain", - tx_templates: &[ - TxTemplate { - tx_name: "A", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10000, Some(0))], - anchors: &[block_id!(1, "B")], - last_seen: None, - ..Default::default() - }, - TxTemplate { - tx_name: "B", - inputs: &[TxInTemplate::PrevTx("A", 0), TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(20000, Some(1))], - last_seen: Some(200), - ..Default::default() - }, - TxTemplate { - tx_name: "B'", - inputs: &[TxInTemplate::PrevTx("A", 0)], - outputs: &[TxOutTemplate::new(50000, Some(4))], - anchors: &[block_id!(1, "B")], - ..Default::default() - }, - TxTemplate { - tx_name: "C", - inputs: &[ - TxInTemplate::PrevTx("B", 0), - TxInTemplate::PrevTx("B'", 0), - ], - outputs: &[TxOutTemplate::new(20000, Some(5))], - ..Default::default() - }, - TxTemplate { - tx_name: "D", - inputs: &[TxInTemplate::PrevTx("C", 0)], - outputs: &[TxOutTemplate::new(20000, Some(6))], - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("A") + .with_inputs(&[TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(0))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("B") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0), TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(1))]) + .with_last_seen(Some(200)), + TxTemplate::new("B'") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(50_000, Some(4))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("C") + .with_inputs(vec![TxInTemplate::PrevTx("B".into(), 0), TxInTemplate::PrevTx("B'".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(5))]), + TxTemplate::new("D") + .with_inputs(vec![TxInTemplate::PrevTx("C".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(6))]), ], // D should not appear in the list methods exp_chain_txs: HashSet::from(["A", "B'"]), @@ -605,26 +430,17 @@ fn test_tx_conflict_handling() { }, Scenario { name: "transitively confirmed ancestors", - tx_templates: &[ - TxTemplate { - tx_name: "first", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(1000, Some(0))], - ..Default::default() - }, - TxTemplate { - tx_name: "second", - inputs: &[TxInTemplate::PrevTx("first", 0)], - outputs: &[TxOutTemplate::new(900, Some(0))], - ..Default::default() - }, - TxTemplate { - tx_name: "anchored", - inputs: &[TxInTemplate::PrevTx("second", 0)], - outputs: &[TxOutTemplate::new(800, Some(0))], - anchors: &[block_id!(3, "D")], - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("first") + .with_inputs(&[TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(1_000, Some(0))]), + TxTemplate::new("second") + .with_inputs(vec![TxInTemplate::PrevTx("first".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(900, Some(0))]), + TxTemplate::new("anchored") + .with_inputs(vec![TxInTemplate::PrevTx("second".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(800, Some(0))]) + .with_anchors(vec![block_id!(3, "D")]), ], exp_chain_txs: HashSet::from(["first", "second", "anchored"]), exp_chain_txouts: HashSet::from([("first", 0), ("second", 0), ("anchored", 0)]), @@ -638,35 +454,23 @@ fn test_tx_conflict_handling() { }, Scenario { name: "transitively anchored txs should have priority over last seen", - tx_templates: &[ - TxTemplate { - tx_name: "root", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10_000, Some(0))], - anchors: &[block_id!(1, "B")], - ..Default::default() - }, - TxTemplate { - tx_name: "last_seen_conflict", - inputs: &[TxInTemplate::PrevTx("root", 0)], - outputs: &[TxOutTemplate::new(9900, Some(1))], - last_seen: Some(1000), - ..Default::default() - }, - TxTemplate { - tx_name: "transitively_anchored_conflict", - inputs: &[TxInTemplate::PrevTx("root", 0)], - outputs: &[TxOutTemplate::new(9000, Some(1))], - last_seen: Some(100), - ..Default::default() - }, - TxTemplate { - tx_name: "anchored", - inputs: &[TxInTemplate::PrevTx("transitively_anchored_conflict", 0)], - outputs: &[TxOutTemplate::new(8000, Some(2))], - anchors: &[block_id!(4, "E")], - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("root") + .with_inputs(&[TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(0))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("last_seen_conflict") + .with_inputs(vec![TxInTemplate::PrevTx("root".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(9_900, Some(1))]) + .with_last_seen(Some(1_000)), + TxTemplate::new("transitively_anchored_conflict") + .with_inputs(vec![TxInTemplate::PrevTx("root".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(9_000, Some(1))]) + .with_last_seen(Some(100)), + TxTemplate::new("anchored") + .with_inputs(vec![TxInTemplate::PrevTx("transitively_anchored_conflict".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(8_000, Some(2))]) + .with_anchors(vec![block_id!(4, "E")]), ], exp_chain_txs: HashSet::from(["root", "transitively_anchored_conflict", "anchored"]), exp_chain_txouts: HashSet::from([("root", 0), ("transitively_anchored_conflict", 0), ("anchored", 0)]), @@ -678,21 +482,15 @@ fn test_tx_conflict_handling() { }, Scenario { name: "tx anchored in orphaned block and not seen in mempool should be canon", - tx_templates: &[ - TxTemplate { - tx_name: "root", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10_000, None)], - anchors: &[block_id!(1, "B")], - ..Default::default() - }, - TxTemplate { - tx_name: "tx", - inputs: &[TxInTemplate::PrevTx("root", 0)], - outputs: &[TxOutTemplate::new(9000, Some(0))], - anchors: &[block_id!(6, "not G")], - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("root") + .with_inputs(vec![TxInTemplate::Bogus ]) + .with_outputs(vec![TxOutTemplate::new(10_000, None)]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("tx") + .with_inputs(vec![TxInTemplate::PrevTx("root".into(), 0) ]) + .with_outputs(vec![TxOutTemplate::new(9_000, Some(0))]) + .with_anchors(vec![block_id!(6, "not G")]), ], exp_chain_txs: HashSet::from(["root", "tx"]), exp_chain_txouts: HashSet::from([("tx", 0)]), @@ -701,28 +499,19 @@ fn test_tx_conflict_handling() { }, Scenario { name: "tx spends from 2 conflicting transactions where a conflict spends another", - tx_templates: &[ - TxTemplate { - tx_name: "A", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10_000, None)], - last_seen: Some(1), - ..Default::default() - }, - TxTemplate { - tx_name: "S1", - inputs: &[TxInTemplate::PrevTx("A", 0)], - outputs: &[TxOutTemplate::new(9_000, None)], - last_seen: Some(2), - ..Default::default() - }, - TxTemplate { - tx_name: "S2", - inputs: &[TxInTemplate::PrevTx("A", 0), TxInTemplate::PrevTx("S1", 0)], - outputs: &[TxOutTemplate::new(17_000, None)], - last_seen: Some(3), - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("A") + .with_inputs(&[TxInTemplate::Bogus ]) + .with_outputs(vec![TxOutTemplate::new(10_000, None)]) + .with_last_seen(Some(1)), + TxTemplate::new("S1") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0) ]) + .with_outputs(vec![TxOutTemplate::new(9_000, None)]) + .with_last_seen(Some(2)), + TxTemplate::new("S2") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0), TxInTemplate::PrevTx("S1".into(), 0) ]) + .with_outputs(vec![TxOutTemplate::new(17_000, None)]) + .with_last_seen(Some(3)), ], exp_chain_txs: HashSet::from(["A", "S1"]), exp_chain_txouts: HashSet::from([]), @@ -731,35 +520,23 @@ fn test_tx_conflict_handling() { }, Scenario { name: "tx spends from 2 conflicting transactions where the conflict is nested", - tx_templates: &[ - TxTemplate { - tx_name: "A", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10_000, Some(0))], - last_seen: Some(1), - ..Default::default() - }, - TxTemplate { - tx_name: "S1", - inputs: &[TxInTemplate::PrevTx("A", 0)], - outputs: &[TxOutTemplate::new(9_000, Some(0))], - last_seen: Some(3), - ..Default::default() - }, - TxTemplate { - tx_name: "B", - inputs: &[TxInTemplate::PrevTx("S1", 0)], - outputs: &[TxOutTemplate::new(8_000, Some(0))], - last_seen: Some(2), - ..Default::default() - }, - TxTemplate { - tx_name: "S2", - inputs: &[TxInTemplate::PrevTx("B", 0), TxInTemplate::PrevTx("A", 0)], - outputs: &[TxOutTemplate::new(17_000, Some(0))], - last_seen: Some(4), - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("A") + .with_inputs(&[TxInTemplate::Bogus ]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(0))]) + .with_last_seen(Some(1)), + TxTemplate::new("S1") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0) ]) + .with_outputs(vec![TxOutTemplate::new(9_000, Some(0))]) + .with_last_seen(Some(3)), + TxTemplate::new("B") + .with_inputs(vec![TxInTemplate::PrevTx("S1".into(), 0) ]) + .with_outputs(vec![TxOutTemplate::new(8_000, Some(0))]) + .with_last_seen(Some(2)), + TxTemplate::new("S2") + .with_inputs(vec![TxInTemplate::PrevTx("B".into(), 0), TxInTemplate::PrevTx("A".into(), 0) ]) + .with_outputs(vec![TxOutTemplate::new(17_000, Some(0))]) + .with_last_seen(Some(4)), ], exp_chain_txs: HashSet::from(["A", "S1", "B"]), exp_chain_txouts: HashSet::from([("A", 0), ("B", 0), ("S1", 0)]), @@ -768,35 +545,23 @@ fn test_tx_conflict_handling() { }, Scenario { name: "tx spends from 2 conflicting transactions where the conflict is nested (different last_seens)", - tx_templates: &[ - TxTemplate { - tx_name: "A", - inputs: &[TxInTemplate::Bogus], - outputs: &[TxOutTemplate::new(10_000, Some(0))], - last_seen: Some(1), - ..Default::default() - }, - TxTemplate { - tx_name: "S1", - inputs: &[TxInTemplate::PrevTx("A", 0)], - outputs: &[TxOutTemplate::new(9_000, Some(0))], - last_seen: Some(4), - ..Default::default() - }, - TxTemplate { - tx_name: "B", - inputs: &[TxInTemplate::PrevTx("S1", 0)], - outputs: &[TxOutTemplate::new(8_000, Some(0))], - last_seen: Some(2), - ..Default::default() - }, - TxTemplate { - tx_name: "S2", - inputs: &[TxInTemplate::PrevTx("A", 0), TxInTemplate::PrevTx("B", 0)], - outputs: &[TxOutTemplate::new(17_000, Some(0))], - last_seen: Some(3), - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("A") + .with_inputs(&[TxInTemplate::Bogus ]) + .with_outputs(vec![TxOutTemplate::new(10_000, Some(0))]) + .with_last_seen(Some(1)), + TxTemplate::new("S1") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0) ]) + .with_outputs(vec![TxOutTemplate::new(9_000, Some(0))]) + .with_last_seen(Some(4)), + TxTemplate::new("B") + .with_inputs(vec![TxInTemplate::PrevTx("S1".into(), 0) ]) + .with_outputs(vec![TxOutTemplate::new(8_000, Some(0))]) + .with_last_seen(Some(2)), + TxTemplate::new("S2") + .with_inputs(vec![TxInTemplate::PrevTx("A".into(), 0), TxInTemplate::PrevTx("B".into(), 0) ]) + .with_outputs(vec![TxOutTemplate::new(17_000, Some(0))]) + .with_last_seen(Some(3)), ], exp_chain_txs: HashSet::from(["A", "S1", "B"]), exp_chain_txouts: HashSet::from([("A", 0), ("B", 0), ("S1", 0)]), @@ -805,41 +570,25 @@ fn test_tx_conflict_handling() { }, Scenario { name: "assume-canonical-tx displaces unconfirmed chain", - tx_templates: &[ - TxTemplate { - tx_name: "root", - inputs: &[TxInTemplate::Bogus], - outputs: &[ - TxOutTemplate::new(21_000, Some(0)), - TxOutTemplate::new(21_000, Some(1)), - ], - anchors: &[block_id!(1, "B")], - ..Default::default() - }, - TxTemplate { - tx_name: "unconfirmed", - inputs: &[TxInTemplate::PrevTx("root", 0)], - outputs: &[TxOutTemplate::new(20_000, Some(1))], - last_seen: Some(2), - ..Default::default() - }, - TxTemplate { - tx_name: "unconfirmed_descendant", - inputs: &[ - TxInTemplate::PrevTx("unconfirmed", 0), - TxInTemplate::PrevTx("root", 1), - ], - outputs: &[TxOutTemplate::new(28_000, Some(2))], - last_seen: Some(2), - ..Default::default() - }, - TxTemplate { - tx_name: "assume_canonical", - inputs: &[TxInTemplate::PrevTx("root", 0)], - outputs: &[TxOutTemplate::new(19_000, Some(3))], - assume_canonical: true, - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("root") + .with_inputs(&[TxInTemplate::Bogus ]) + .with_outputs(vec![TxOutTemplate::new(21_000, Some(0)), + TxOutTemplate::new(21_000, Some(1))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("unconfirmed") + .with_inputs(vec![TxInTemplate::PrevTx("root".into(), 0) ]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(1))]) + .with_last_seen(Some(2)), + TxTemplate::new("unconfirmed_descendant") + .with_inputs(vec![TxInTemplate::PrevTx("unconfirmed".into(), 0), + TxInTemplate::PrevTx("root".into(), 1) ]) + .with_outputs(vec![TxOutTemplate::new(28_000, Some(2))]) + .with_last_seen(Some(2)), + TxTemplate::new("assume_canonical") + .with_inputs(vec![TxInTemplate::PrevTx("root".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(19_000, Some(3))]) + .with_assume_canonical(true), ], exp_chain_txs: HashSet::from(["root", "assume_canonical"]), exp_chain_txouts: HashSet::from([("root", 0), ("root", 1), ("assume_canonical", 0)]), @@ -853,41 +602,24 @@ fn test_tx_conflict_handling() { }, Scenario { name: "assume-canonical-tx displaces confirmed chain", - tx_templates: &[ - TxTemplate { - tx_name: "root", - inputs: &[TxInTemplate::Bogus], - outputs: &[ - TxOutTemplate::new(21_000, Some(0)), - TxOutTemplate::new(21_000, Some(1)), - ], - anchors: &[block_id!(1, "B")], - ..Default::default() - }, - TxTemplate { - tx_name: "confirmed", - inputs: &[TxInTemplate::PrevTx("root", 0)], - outputs: &[TxOutTemplate::new(20_000, Some(1))], - anchors: &[block_id!(2, "C")], - ..Default::default() - }, - TxTemplate { - tx_name: "confirmed_descendant", - inputs: &[ - TxInTemplate::PrevTx("confirmed", 0), - TxInTemplate::PrevTx("root", 1), - ], - outputs: &[TxOutTemplate::new(28_000, Some(2))], - anchors: &[block_id!(3, "D")], - ..Default::default() - }, - TxTemplate { - tx_name: "assume_canonical", - inputs: &[TxInTemplate::PrevTx("root", 0)], - outputs: &[TxOutTemplate::new(19_000, Some(3))], - assume_canonical: true, - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("root") + .with_inputs(&[TxInTemplate::Bogus ]) + .with_outputs(vec![TxOutTemplate::new(21_000, Some(0)), + TxOutTemplate::new(21_000, Some(1))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("confirmed") + .with_inputs(vec![TxInTemplate::PrevTx("root".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(1))]) + .with_anchors(vec![block_id!(2, "C")]), + TxTemplate::new("confirmed_descendant") + .with_inputs(vec![TxInTemplate::PrevTx("confirmed".into(), 0), TxInTemplate::PrevTx("root".into(), 1)]) + .with_outputs(vec![TxOutTemplate::new(28_000, Some(2))]) + .with_anchors(vec![block_id!(3, "D")]), + TxTemplate::new("assume_canonical") + .with_inputs(vec![TxInTemplate::PrevTx("root".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(19_000, Some(3))]) + .with_assume_canonical(true), ], exp_chain_txs: HashSet::from(["root", "assume_canonical"]), exp_chain_txouts: HashSet::from([("root", 0), ("root", 1), ("assume_canonical", 0)]), @@ -901,37 +633,23 @@ fn test_tx_conflict_handling() { }, Scenario { name: "assume-canonical txs respects order", - tx_templates: &[ - TxTemplate { - tx_name: "root", - inputs: &[TxInTemplate::Bogus], - outputs: &[ - TxOutTemplate::new(21_000, Some(0)), - ], - anchors: &[block_id!(1, "B")], - ..Default::default() - }, - TxTemplate { - tx_name: "assume_a", - inputs: &[TxInTemplate::PrevTx("root", 0)], - outputs: &[TxOutTemplate::new(20_000, Some(1))], - assume_canonical: true, - ..Default::default() - }, - TxTemplate { - tx_name: "assume_b", - inputs: &[TxInTemplate::PrevTx("root", 0)], - outputs: &[TxOutTemplate::new(19_000, Some(1))], - assume_canonical: true, - ..Default::default() - }, - TxTemplate { - tx_name: "assume_c", - inputs: &[TxInTemplate::PrevTx("root", 0)], - outputs: &[TxOutTemplate::new(18_000, Some(1))], - assume_canonical: true, - ..Default::default() - }, + tx_templates: vec![ + TxTemplate::new("root") + .with_inputs(&[TxInTemplate::Bogus]) + .with_outputs(vec![TxOutTemplate::new(21_000, Some(0))]) + .with_anchors(vec![block_id!(1, "B")]), + TxTemplate::new("assume_a") + .with_inputs(vec![TxInTemplate::PrevTx("root".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(20_000, Some(1))]) + .with_assume_canonical(true), + TxTemplate::new("assume_b") + .with_inputs(vec![TxInTemplate::PrevTx("root".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(19_000, Some(1))]) + .with_assume_canonical(true), + TxTemplate::new("assume_c") + .with_inputs(vec![TxInTemplate::PrevTx("root".into(), 0)]) + .with_outputs(vec![TxOutTemplate::new(18_000, Some(1))]) + .with_assume_canonical(true), ], exp_chain_txs: HashSet::from(["root", "assume_c"]), exp_chain_txouts: HashSet::from([("root", 0), ("assume_c", 0)]), @@ -945,15 +663,11 @@ fn test_tx_conflict_handling() { }, Scenario { name: "coinbase tx must not become unconfirmed", - tx_templates: &[ - TxTemplate { - tx_name: "coinbase", - inputs: &[TxInTemplate::Coinbase], - outputs: &[TxOutTemplate::new(21_000, Some(0))], - // Stale block - anchors: &[block_id!(1, "B-prime")], - ..Default::default() - } + tx_templates: vec![ + TxTemplate::new("coinbase") + .with_inputs(&[TxInTemplate::Coinbase]) + .with_outputs(vec![TxOutTemplate::new(21_000, Some(0))]) + .with_anchors(vec![block_id!(1, "B-prime")]) ], exp_chain_txs: HashSet::from([]), exp_chain_txouts: HashSet::from([]), @@ -968,7 +682,7 @@ fn test_tx_conflict_handling() { ]; for scenario in scenarios { - let env = init_graph(scenario.tx_templates.iter()); + let env = init_graph(scenario.tx_templates); let txs = env .tx_graph @@ -979,7 +693,7 @@ fn test_tx_conflict_handling() { let exp_txs = scenario .exp_chain_txs .iter() - .map(|txid| *env.txid_to_name.get(txid).expect("txid must exist")) + .map(|&txid| *env.txid_to_name.get(txid).expect("txid must exist")) .collect::>(); assert_eq!( txs, exp_txs, @@ -996,9 +710,9 @@ fn test_tx_conflict_handling() { let exp_txouts = scenario .exp_chain_txouts .iter() - .map(|(txid, vout)| OutPoint { + .map(|&(txid, vout)| OutPoint { txid: *env.txid_to_name.get(txid).expect("txid must exist"), - vout: *vout, + vout, }) .collect::>(); assert_eq!( @@ -1016,9 +730,9 @@ fn test_tx_conflict_handling() { let exp_utxos = scenario .exp_unspents .iter() - .map(|(txid, vout)| OutPoint { + .map(|&(txid, vout)| OutPoint { txid: *env.txid_to_name.get(txid).expect("txid must exist"), - vout: *vout, + vout, }) .collect::>(); assert_eq!( diff --git a/crates/testenv/Cargo.toml b/crates/testenv/Cargo.toml index d24ae7e5fd..f0f9e623fa 100644 --- a/crates/testenv/Cargo.toml +++ b/crates/testenv/Cargo.toml @@ -16,9 +16,10 @@ readme = "README.md" workspace = true [dependencies] -bdk_chain = { path = "../chain", version = "0.23.1", default-features = false } +bdk_chain = { path = "../chain", version = "0.23.1", default-features = false, features = ["miniscript"]} electrsd = { version = "0.36.1", features = [ "legacy" ], default-features = false } bitcoin = { version = "0.32.0", default-features = false } +rand = { version = "0.8.0", default-features = false, features = ["std", "small_rng", "std_rng"] } [dev-dependencies] bdk_testenv = { path = "." } diff --git a/crates/testenv/src/lib.rs b/crates/testenv/src/lib.rs index eec99efa81..087fc8aef7 100644 --- a/crates/testenv/src/lib.rs +++ b/crates/testenv/src/lib.rs @@ -1,6 +1,8 @@ #![cfg_attr(coverage_nightly, feature(coverage_attribute))] +pub mod tx_template; pub mod utils; +pub use tx_template::*; use anyhow::Context; use bdk_chain::bitcoin::{ diff --git a/crates/testenv/src/tx_template.rs b/crates/testenv/src/tx_template.rs new file mode 100644 index 0000000000..c75e6cf9c0 --- /dev/null +++ b/crates/testenv/src/tx_template.rs @@ -0,0 +1,258 @@ +//! Transaction templates for constructing complex transaction histories for testing purposes. + +use crate::utils::DESCRIPTORS; +use bdk_chain::{ + miniscript::Descriptor, spk_txout::SpkTxOutIndex, tx_graph::TxGraph, Anchor, + CanonicalizationParams, +}; +use bitcoin::{ + locktime::absolute::LockTime, secp256k1::Secp256k1, transaction, Amount, OutPoint, ScriptBuf, + Sequence, Transaction, TxIn, TxOut, Txid, Witness, +}; +use rand::distributions::{Alphanumeric, DistString}; +use std::{borrow::Cow, collections::HashMap}; + +/// Template for creating a transaction in a [`TxGraph`]. +/// +/// This is the main building block for constructing complex transaction histories +/// for tests. It allows you to refer to previous transactions by name instead of +/// manually managing txids and outpoints. +#[derive(Clone)] +pub struct TxTemplate +where + A: Clone + 'static, +{ + /// A unique name used to refer to this transaction in other templates. + pub tx_name: Cow<'static, str>, + + /// The inputs of this transaction. + pub inputs: Cow<'static, [TxInTemplate]>, + + /// The outputs of this transaction. + pub outputs: Cow<'static, [TxOutTemplate]>, + + /// Anchors (confirmations) for this transaction. + pub anchors: Cow<'static, [A]>, + + /// Unix timestamp when this transaction was last seen in the mempool. + pub last_seen: Option, + + /// If `true`, this transaction will be treated as canonical regardless of + /// conflict resolution rules (used for testing forced canonicalization). + pub assume_canonical: bool, +} + +impl Default for TxTemplate +where + A: Clone + 'static, +{ + fn default() -> Self { + Self { + tx_name: Cow::Borrowed(""), + inputs: Cow::Borrowed(&[]), + outputs: Cow::Borrowed(&[]), + anchors: Cow::Borrowed(&[]), + last_seen: None, + assume_canonical: false, + } + } +} + +impl TxTemplate +where + A: Clone + 'static, +{ + /// Create a new template with a name. + pub fn new(name: impl Into>) -> Self { + Self { + tx_name: name.into(), + ..Default::default() + } + } + + //// Set inputs. Accepts `&[TxInTemplate]`, `Vec`, or a static slice. + pub fn with_inputs(mut self, inputs: impl Into>) -> Self { + self.inputs = inputs.into(); + self + } + + /// Set outputs with `Vec` or `&'static [TxOutTemplate]`. + pub fn with_outputs(mut self, outputs: impl Into>) -> Self { + self.outputs = outputs.into(); + self + } + + /// Set anchors. Supports `Vec` or `&'static [A]`. + pub fn with_anchors(mut self, anchors: impl Into>) -> Self { + self.anchors = anchors.into(); + self + } + + /// Mark this transaction as canonical. + pub fn with_assume_canonical(mut self, assume: bool) -> Self { + self.assume_canonical = assume; + self + } + + /// Set the last-seen mempool timestamp. + pub fn with_last_seen(mut self, last_seen: Option) -> Self { + self.last_seen = last_seen; + self + } +} + +/// Describes how an input is created in a [`TxTemplate`]. +#[derive(Clone, Debug)] +#[allow(dead_code)] +pub enum TxInTemplate { + /// A random (bogus) previous output. Useful when the actual prevout doesn't matter. + Bogus, + + /// A coinbase input (no previous output). + Coinbase, + + /// Spends from a previous transaction defined in the template list. + /// + /// The rule is that the referenced transaction (`prev_name`) must appear + /// earlier in the list passed to [`init_graph`]. + PrevTx(Cow<'static, str>, usize), +} + +/// Describes an output in a [`TxTemplate`]. +#[derive(Clone, Debug)] +pub struct TxOutTemplate { + /// Value in satoshis. + pub value: u64, + /// If `Some(index)`, the output will use the script pubkey at that index + /// from the test descriptor set. If `None`, a random (empty) script is used. + pub spk_index: Option, +} + +impl TxOutTemplate { + pub fn new(value: u64, spk_index: Option) -> Self { + TxOutTemplate { value, spk_index } + } +} + +/// The result of calling [`init_graph`]. +/// +/// Contains the built [`TxGraph`], the associated indexer, and a mapping from +/// template names to their final txids. +#[allow(dead_code)] +pub struct TxTemplateEnv { + pub tx_graph: TxGraph, + pub indexer: SpkTxOutIndex, + pub txid_to_name: HashMap, Txid>, + pub canonicalization_params: CanonicalizationParams, +} + +/// Builds a [`TxGraph`] (and associated indexer) from a list of [`TxTemplate`]s. +/// +/// This is the main entry point for using transaction templates in tests. +/// It handles txid generation, outpoint wiring, anchor insertion, and last-seen +/// timestamps automatically. +pub fn init_graph( + tx_templates: impl IntoIterator>, +) -> TxTemplateEnv { + let (descriptor, _) = + Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[2]).unwrap(); + + let mut tx_graph = TxGraph::::default(); + let mut indexer = SpkTxOutIndex::default(); + + // Pre-populate the indexer with 10 script pubkeys from the test descriptor + (0..10).for_each(|index| { + indexer.insert_spk( + index, + descriptor + .at_derivation_index(index) + .unwrap() + .script_pubkey(), + ); + }); + + let mut txid_to_name = HashMap::, Txid>::new(); + let mut canonicalization_params = CanonicalizationParams::default(); + + for (bogus_txin_vout, tx_tmp) in tx_templates.into_iter().enumerate() { + let tx = Transaction { + version: transaction::Version::non_standard(0), + lock_time: LockTime::ZERO, + input: tx_tmp + .inputs + .iter() + .map(|input| match input { + TxInTemplate::Bogus => TxIn { + previous_output: OutPoint::new( + bitcoin::hashes::Hash::hash( + Alphanumeric + .sample_string(&mut rand::thread_rng(), 20) + .as_bytes(), + ), + bogus_txin_vout as u32, + ), + script_sig: ScriptBuf::new(), + sequence: Sequence::default(), + witness: Witness::new(), + }, + TxInTemplate::Coinbase => TxIn { + previous_output: OutPoint::null(), + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + }, + TxInTemplate::PrevTx(prev_name, prev_vout) => { + let prev_txid = txid_to_name.get(prev_name).expect( + "txin template must spend from tx of template that comes before", + ); + TxIn { + previous_output: OutPoint::new(*prev_txid, *prev_vout as _), + script_sig: ScriptBuf::new(), + sequence: Sequence::default(), + witness: Witness::new(), + } + } + }) + .collect(), + output: tx_tmp + .outputs + .iter() + .map(|output| match &output.spk_index { + None => TxOut { + value: Amount::from_sat(output.value), + script_pubkey: ScriptBuf::new(), + }, + Some(index) => TxOut { + value: Amount::from_sat(output.value), + script_pubkey: indexer.spk_at_index(index).unwrap(), + }, + }) + .collect(), + }; + + let txid = tx.compute_txid(); + + if tx_tmp.assume_canonical { + canonicalization_params.assume_canonical.push(txid); + } + + txid_to_name.insert(tx_tmp.tx_name, txid); + indexer.scan(&tx); + let _ = tx_graph.insert_tx(tx.clone()); + + for anchor in tx_tmp.anchors.iter() { + let _ = tx_graph.insert_anchor(txid, anchor.clone()); + } + + if let Some(last_seen) = tx_tmp.last_seen { + let _ = tx_graph.insert_seen_at(txid, last_seen); + } + } + + TxTemplateEnv { + tx_graph, + indexer, + txid_to_name, + canonicalization_params, + } +} diff --git a/crates/testenv/src/utils.rs b/crates/testenv/src/utils.rs index 93ca1f217f..fb5e37f4c4 100644 --- a/crates/testenv/src/utils.rs +++ b/crates/testenv/src/utils.rs @@ -1,4 +1,12 @@ -use bdk_chain::bitcoin; +use bdk_chain::{ + bitcoin, + miniscript::{Descriptor, DescriptorPublicKey}, + BlockId, +}; +use bitcoin::{ + absolute::LockTime, constants, hashes::Hash, key::Secp256k1, transaction::Version, BlockHash, + Network, ScriptBuf, Transaction, TxOut, +}; #[allow(unused_macros)] #[macro_export] @@ -67,9 +75,22 @@ macro_rules! changeset { }}; } +/// Generate a dummy script pubkey. +#[allow(unused_macros)] +#[macro_export] +macro_rules! spk { + () => {{ + let secp = bitcoin::secp256k1::Secp256k1::new(); + let (x_only_pk, _) = bitcoin::secp256k1::SecretKey::new(&mut rand::thread_rng()) + .public_key(&secp) + .x_only_public_key(); + bitcoin::ScriptBuf::new_p2tr(&secp, x_only_pk, None) + }}; +} + #[allow(unused)] -pub fn new_tx(lt: u32) -> bitcoin::Transaction { - bitcoin::Transaction { +pub fn new_tx(lt: u32) -> Transaction { + Transaction { version: bitcoin::transaction::Version::non_standard(0x00), lock_time: bitcoin::absolute::LockTime::from_consensus(lt), input: vec![], @@ -77,6 +98,38 @@ pub fn new_tx(lt: u32) -> bitcoin::Transaction { } } +/// Initialize a standard transaction with a guaranteed output. +pub fn new_standard_tx(lt: u32) -> Transaction { + Transaction { + version: Version::TWO, + lock_time: LockTime::from_consensus(lt), + input: vec![], + output: vec![TxOut::NULL], + } +} + +pub fn genesis_block_id() -> BlockId { + BlockId { + height: 0, + hash: constants::genesis_block(Network::Regtest).block_hash(), + } +} + +pub fn tip_block_id(height: u32) -> BlockId { + BlockId { + height, + hash: BlockHash::all_zeros(), + } +} + +/// Derives a [`ScriptBuf`] (scriptPubkey) from the provided descriptor at a specific index. +pub fn spk_at_index(descriptor: &Descriptor, index: u32) -> ScriptBuf { + descriptor + .derived_descriptor(&Secp256k1::verification_only(), index) + .expect("must derive") + .script_pubkey() +} + #[allow(unused)] pub const DESCRIPTORS: [&str; 7] = [ "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)",