From 064698002a0e93d1a368e57424c37066bcd5d40a Mon Sep 17 00:00:00 2001 From: valued mammal Date: Sat, 16 Nov 2024 20:23:59 -0500 Subject: [PATCH 1/3] chore: add crate `bench` to workspace chain: add optional dep `criterion`. Ideally this should be conditional on "--cfg=bdk_bench". See LDK for example --- .gitignore | 2 +- Cargo.toml | 1 + bench/Cargo.toml | 12 ++++ bench/README.md | 3 + bench/benches/bench.rs | 7 +++ crates/chain/Cargo.toml | 1 + crates/chain/src/tx_graph.rs | 112 +++++++++++++++++++++++++++++++++++ 7 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 bench/Cargo.toml create mode 100644 bench/README.md create mode 100644 bench/benches/bench.rs diff --git a/.gitignore b/.gitignore index e2d4d770a..7454134f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/target +**/target Cargo.lock /.vscode diff --git a/Cargo.toml b/Cargo.toml index 2abc16bd8..16eb439b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "example-crates/example_wallet_esplora_async", "example-crates/example_wallet_rpc", ] +exclude = ["bench"] [workspace.package] authors = ["Bitcoin Dev Kit Developers"] diff --git a/bench/Cargo.toml b/bench/Cargo.toml new file mode 100644 index 000000000..e287864db --- /dev/null +++ b/bench/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "bdk_bench" +version = "0.1.0" +edition = "2021" + +[[bench]] +name = "bench" +harness = false + +[dependencies] +bdk_chain = { path = "../crates/chain" } +criterion = { version = "0.5", default-features = false } diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 000000000..c4726fc91 --- /dev/null +++ b/bench/README.md @@ -0,0 +1,3 @@ +# To run benches + +`cargo bench` diff --git a/bench/benches/bench.rs b/bench/benches/bench.rs new file mode 100644 index 000000000..3c5c66d47 --- /dev/null +++ b/bench/benches/bench.rs @@ -0,0 +1,7 @@ +extern crate bdk_chain; +extern crate criterion; + +use criterion::{criterion_group, criterion_main}; + +criterion_group!(benches, bdk_chain::tx_graph::bench::filter_chain_unspents); +criterion_main!(benches); diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index cfdaa1cab..c01452a03 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -20,6 +20,7 @@ bitcoin = { version = "0.32.0", default-features = false } bdk_core = { path = "../core", version = "0.3.0", default-features = false } serde = { version = "1", optional = true, features = ["derive", "rc"] } miniscript = { version = "12.0.0", optional = true, default-features = false } +criterion = { version = "0.5", default-features = false } # Feature dependencies rusqlite = { version = "0.31.0", features = ["bundled"], optional = true } diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index a10d1aeb8..ac15097b9 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -1553,3 +1553,115 @@ where fn tx_outpoint_range(txid: Txid) -> RangeInclusive { OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX) } + +/// Bench +#[allow(unused)] +#[allow(missing_docs)] +pub mod bench { + use std::str::FromStr; + + use bdk_core::{CheckPoint, ConfirmationBlockTime}; + use bitcoin::absolute; + use bitcoin::hashes::Hash; + use bitcoin::transaction; + use bitcoin::{Address, BlockHash, Network, TxIn}; + use criterion::Criterion; + use miniscript::Descriptor; + use miniscript::DescriptorPublicKey; + + use super::*; + use crate::keychain_txout::KeychainTxOutIndex; + use crate::local_chain::LocalChain; + use crate::IndexedTxGraph; + + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + enum Keychain { + External, + } + + const EXTERNAL: &str = "tr([ab28dc00/86h/1h/0h]tpubDCdDtzAMZZrkwKBxwNcGCqe4FRydeD9rfMisoi7qLdraG79YohRfPW4YgdKQhpgASdvh612xXNY5xYzoqnyCgPbkpK4LSVcH5Xv4cK7johH/0/*)"; + + fn get_params() -> ( + IndexedTxGraph>, + LocalChain, + ) { + let genesis = bitcoin::constants::genesis_block(Network::Regtest).block_hash(); + let block_0 = BlockId { + height: 0, + hash: genesis, + }; + let mut cp = CheckPoint::new(block_0); + let block_100 = BlockId { + height: 100, + hash: BlockHash::all_zeros(), + }; + cp = cp.push(block_100).unwrap(); + let chain = LocalChain::from_tip(cp).unwrap(); + + let mut graph = IndexedTxGraph::new({ + let mut index = KeychainTxOutIndex::new(10); + index + .insert_descriptor(Keychain::External, parse_descriptor(EXTERNAL)) + .unwrap(); + index + }); + + // insert funding tx (coinbase) + let addr_0 = + Address::from_str("bcrt1plhmjhj75nut38qwwm5w7xqysy25xhd4ckuv7zu5tey3nkmcwh3cqvan5mz") + .unwrap() + .assume_checked(); + let tx_0 = Transaction { + output: vec![TxOut { + script_pubkey: addr_0.script_pubkey(), + value: Amount::ONE_BTC, + }], + ..new_tx(0) + }; + let txid_0 = tx_0.compute_txid(); + let _ = graph.insert_tx(tx_0); + let _ = graph.insert_anchor( + txid_0, + ConfirmationBlockTime { + block_id: block_100, + confirmation_time: 100, + }, + ); + + (graph, chain) + } + + fn parse_descriptor(s: &str) -> miniscript::Descriptor { + >::parse_descriptor( + &bitcoin::secp256k1::Secp256k1::new(), + s, + ) + .unwrap() + .0 + } + + fn new_tx(lt: u32) -> Transaction { + Transaction { + version: transaction::Version::TWO, + lock_time: absolute::LockTime::from_consensus(lt), + input: vec![], + output: vec![], + } + } + + pub fn filter_chain_unspents(bench: &mut Criterion) { + let (graph, chain) = get_params(); + // TODO: insert conflicts + let outpoints = graph.index.outpoints().clone(); + bench.bench_function("filter_chain_unspents", |b| { + b.iter(|| { + TxGraph::filter_chain_unspents( + graph.graph(), + &chain, + chain.tip().block_id(), + outpoints.clone(), + ) + }) + }); + } +} From 27a0c9b6e10acfb8168d676d12600370292dc930 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Sat, 16 Nov 2024 21:07:10 -0500 Subject: [PATCH 2/3] chore: gate criterion benches behind `cfg(bdk_bench)` ci: bump nightly docs toolchain to 2024-11-17 --- .github/workflows/nightly_docs.yml | 2 +- Cargo.toml | 6 ++++++ bench/Cargo.toml | 2 +- bench/README.md | 6 ++++-- crates/chain/Cargo.toml | 4 +++- crates/chain/src/lib.rs | 2 ++ crates/chain/src/tx_graph.rs | 4 ++-- 7 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/nightly_docs.yml b/.github/workflows/nightly_docs.yml index 0bbe49936..75e25f3d3 100644 --- a/.github/workflows/nightly_docs.yml +++ b/.github/workflows/nightly_docs.yml @@ -10,7 +10,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v4 - name: Set default toolchain - run: rustup default nightly-2024-05-12 + run: rustup default nightly-2024-11-17 - name: Set profile run: rustup set profile minimal - name: Update toolchain diff --git a/Cargo.toml b/Cargo.toml index 16eb439b5..0c486831f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,3 +26,9 @@ authors = ["Bitcoin Dev Kit Developers"] [workspace.lints.clippy] print_stdout = "deny" print_stderr = "deny" + +[workspace.lints.rust.unexpected_cfgs] +level = "forbid" +check-cfg = [ + "cfg(bdk_bench)", +] diff --git a/bench/Cargo.toml b/bench/Cargo.toml index e287864db..cca925674 100644 --- a/bench/Cargo.toml +++ b/bench/Cargo.toml @@ -8,5 +8,5 @@ name = "bench" harness = false [dependencies] -bdk_chain = { path = "../crates/chain" } +bdk_chain = { path = "../crates/chain", features = ["criterion"] } criterion = { version = "0.5", default-features = false } diff --git a/bench/README.md b/bench/README.md index c4726fc91..82415dd23 100644 --- a/bench/README.md +++ b/bench/README.md @@ -1,3 +1,5 @@ -# To run benches +# BDK bench -`cargo bench` +To run benchmarks in the current directory: + +`RUSTFLAGS="--cfg=bdk_bench" cargo bench` diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index c01452a03..ec6f04aa2 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -20,7 +20,6 @@ bitcoin = { version = "0.32.0", default-features = false } bdk_core = { path = "../core", version = "0.3.0", default-features = false } serde = { version = "1", optional = true, features = ["derive", "rc"] } miniscript = { version = "12.0.0", optional = true, default-features = false } -criterion = { version = "0.5", default-features = false } # Feature dependencies rusqlite = { version = "0.31.0", features = ["bundled"], optional = true } @@ -31,6 +30,9 @@ rand = "0.8" proptest = "1.2.0" bdk_testenv = { path = "../testenv", default-features = false } +[target.'cfg(bdk_bench)'.dependencies] +criterion = { version = "0.5", optional = true, default-features = false } + [features] default = ["std", "miniscript"] diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 9667bb549..36b50c8d9 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -68,6 +68,8 @@ pub use bdk_core::*; #[allow(unused_imports)] #[macro_use] extern crate alloc; +#[cfg(bdk_bench)] +extern crate criterion; #[cfg(feature = "rusqlite")] pub extern crate rusqlite; #[cfg(feature = "serde")] diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index ac15097b9..665f2f3f8 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -1557,6 +1557,7 @@ fn tx_outpoint_range(txid: Txid) -> RangeInclusive { /// Bench #[allow(unused)] #[allow(missing_docs)] +#[cfg(bdk_bench)] pub mod bench { use std::str::FromStr; @@ -1652,14 +1653,13 @@ pub mod bench { pub fn filter_chain_unspents(bench: &mut Criterion) { let (graph, chain) = get_params(); // TODO: insert conflicts - let outpoints = graph.index.outpoints().clone(); bench.bench_function("filter_chain_unspents", |b| { b.iter(|| { TxGraph::filter_chain_unspents( graph.graph(), &chain, chain.tip().block_id(), - outpoints.clone(), + graph.index.outpoints().clone(), ) }) }); From a65040847e6ec9816e97c4fd876975ddcaaea3d5 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Sun, 17 Nov 2024 23:17:08 -0500 Subject: [PATCH 3/3] wip: add bench tests --- bench/benches/bench.rs | 6 +- crates/chain/src/tx_graph.rs | 316 ++++++++++++++++++++++++++++------- 2 files changed, 261 insertions(+), 61 deletions(-) diff --git a/bench/benches/bench.rs b/bench/benches/bench.rs index 3c5c66d47..9ee5103da 100644 --- a/bench/benches/bench.rs +++ b/bench/benches/bench.rs @@ -3,5 +3,9 @@ extern crate criterion; use criterion::{criterion_group, criterion_main}; -criterion_group!(benches, bdk_chain::tx_graph::bench::filter_chain_unspents); +criterion_group!(benches, + bdk_chain::tx_graph::bench::filter_chain_unspents, + bdk_chain::tx_graph::bench::list_canonical_txs, + bdk_chain::tx_graph::bench::nested_conflicts, +); criterion_main!(benches); diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 665f2f3f8..aa5761e5b 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -1554,21 +1554,14 @@ fn tx_outpoint_range(txid: Txid) -> RangeInclusive { OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX) } -/// Bench -#[allow(unused)] -#[allow(missing_docs)] -#[cfg(bdk_bench)] -pub mod bench { - use std::str::FromStr; - +#[cfg(any(test, bdk_bench))] +mod bench_util { use bdk_core::{CheckPoint, ConfirmationBlockTime}; - use bitcoin::absolute; - use bitcoin::hashes::Hash; - use bitcoin::transaction; - use bitcoin::{Address, BlockHash, Network, TxIn}; - use criterion::Criterion; - use miniscript::Descriptor; - use miniscript::DescriptorPublicKey; + use bitcoin::{ + absolute, constants, hashes::Hash, secp256k1::Secp256k1, transaction, BlockHash, Network, + TxIn, + }; + use miniscript::{Descriptor, DescriptorPublicKey}; use super::*; use crate::keychain_txout::KeychainTxOutIndex; @@ -1576,91 +1569,294 @@ pub mod bench { use crate::IndexedTxGraph; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] - enum Keychain { + pub enum Keychain { External, } const EXTERNAL: &str = "tr([ab28dc00/86h/1h/0h]tpubDCdDtzAMZZrkwKBxwNcGCqe4FRydeD9rfMisoi7qLdraG79YohRfPW4YgdKQhpgASdvh612xXNY5xYzoqnyCgPbkpK4LSVcH5Xv4cK7johH/0/*)"; - fn get_params() -> ( - IndexedTxGraph>, - LocalChain, - ) { - let genesis = bitcoin::constants::genesis_block(Network::Regtest).block_hash(); + pub fn parse_descriptor(s: &str) -> Descriptor { + >::parse_descriptor(&Secp256k1::new(), s) + .unwrap() + .0 + } + + /// New tx guaranteed to have at least one output + pub fn new_tx(lt: u32) -> Transaction { + Transaction { + version: transaction::Version::TWO, + lock_time: absolute::LockTime::from_consensus(lt), + input: vec![], + output: vec![TxOut::NULL], + } + } + + pub fn spk_at_index(txout_index: &KeychainTxOutIndex, index: u32) -> ScriptBuf { + txout_index + .get_descriptor(Keychain::External) + .unwrap() + .at_derivation_index(index) + .unwrap() + .script_pubkey() + } + + type KeychainTxGraph = IndexedTxGraph>; + + /// Initialize indexed tx-graph with one keychain and a local chain. Also insert the + /// first ancestor tx. + pub fn init_graph_chain() -> (KeychainTxGraph, LocalChain) { + let genesis = constants::genesis_block(Network::Regtest).block_hash(); let block_0 = BlockId { height: 0, hash: genesis, }; + // chain tip 100 let mut cp = CheckPoint::new(block_0); - let block_100 = BlockId { + let chain_tip = BlockId { height: 100, hash: BlockHash::all_zeros(), }; - cp = cp.push(block_100).unwrap(); + cp = cp.push(chain_tip).unwrap(); let chain = LocalChain::from_tip(cp).unwrap(); - let mut graph = IndexedTxGraph::new({ - let mut index = KeychainTxOutIndex::new(10); - index - .insert_descriptor(Keychain::External, parse_descriptor(EXTERNAL)) - .unwrap(); - index - }); + let desc = parse_descriptor(EXTERNAL); + let mut index = KeychainTxOutIndex::new(10); + index.insert_descriptor(Keychain::External, desc).unwrap(); + let mut graph = IndexedTxGraph::new(index); - // insert funding tx (coinbase) - let addr_0 = - Address::from_str("bcrt1plhmjhj75nut38qwwm5w7xqysy25xhd4ckuv7zu5tey3nkmcwh3cqvan5mz") - .unwrap() - .assume_checked(); - let tx_0 = Transaction { + // insert funding tx (coinbase) confirmed at chain tip + add_ancestor_tx(&mut graph, chain_tip, 0); + + (graph, chain) + } + + /// Add ancestor tx confirmed at `block_id` with `locktime` (used for uniqueness). + /// The transaction always pays 1 BTC to SPK 0. + pub fn add_ancestor_tx(graph: &mut KeychainTxGraph, block_id: BlockId, locktime: u32) { + let spk_0 = spk_at_index(&graph.index, 0); + let tx = Transaction { + input: vec![TxIn::default()], output: vec![TxOut { - script_pubkey: addr_0.script_pubkey(), value: Amount::ONE_BTC, + script_pubkey: spk_0, }], - ..new_tx(0) + ..new_tx(locktime) }; - let txid_0 = tx_0.compute_txid(); - let _ = graph.insert_tx(tx_0); + let txid = tx.compute_txid(); + let _ = graph.insert_tx(tx); let _ = graph.insert_anchor( - txid_0, + txid, ConfirmationBlockTime { - block_id: block_100, + block_id, confirmation_time: 100, }, ); + } - (graph, chain) + /// Add `n` conflicts to `graph` that spend the given `previous_output`, incrementing + /// the tx last-seen each time. + pub fn add_conflicts(n: u32, graph: &mut KeychainTxGraph, previous_output: OutPoint) { + let spk_1 = spk_at_index(&graph.index, 1); + for i in 1..n + 1 { + let tx = Transaction { + input: vec![TxIn { + previous_output, + ..Default::default() + }], + output: vec![TxOut { + value: Amount::ONE_BTC - Amount::from_sat(i as u64 * 10), + script_pubkey: spk_1.clone(), + }], + ..new_tx(i) + }; + let update = TxUpdate { + txs: vec![Arc::new(tx)], + ..Default::default() + }; + let _ = graph.apply_update_at(update, Some(i as u64)); + } } - fn parse_descriptor(s: &str) -> miniscript::Descriptor { - >::parse_descriptor( - &bitcoin::secp256k1::Secp256k1::new(), - s, - ) - .unwrap() - .0 + /// Apply a chain of `n` unconfirmed txs where each subsequent tx spends the output + /// of the previous one. + pub fn chain_unconfirmed(n: u32, graph: &mut KeychainTxGraph, mut previous_output: OutPoint) { + for i in 0..n { + // create tx + let tx = Transaction { + input: vec![TxIn { + previous_output, + ..Default::default() + }], + ..new_tx(i) + }; + let txid = tx.compute_txid(); + let update = TxUpdate { + txs: vec![Arc::new(tx)], + ..Default::default() + }; + let _ = graph.apply_update_at(update, Some(21)); + // store the next prevout + previous_output = OutPoint::new(txid, 0); + } } - fn new_tx(lt: u32) -> Transaction { - Transaction { - version: transaction::Version::TWO, - lock_time: absolute::LockTime::from_consensus(lt), - input: vec![], - output: vec![], + /// Insert `n` txs where + /// - half spend ancestor A + /// - half spend ancestor B + /// - and one spends both + pub fn add_nested_conflicts(n: u32, graph: &mut KeychainTxGraph, chain: &LocalChain) { + // add ancestor B + add_ancestor_tx(graph, chain.tip().block_id(), 1); + + let outpoints: Vec<_> = graph.index.outpoints().iter().map(|(_, op)| *op).collect(); + assert!(outpoints.len() >= 2); + let op_a = outpoints[0]; + let op_b = outpoints[1]; + + for i in 0..n { + let tx = if i == n / 2 { + // tx spends both A, B + Transaction { + input: vec![ + TxIn { + previous_output: op_a, + ..Default::default() + }, + TxIn { + previous_output: op_b, + ..Default::default() + }, + ], + ..new_tx(i) + } + } else if i % 2 == 1 { + // tx spends A + Transaction { + input: vec![TxIn { + previous_output: op_a, + ..Default::default() + }], + ..new_tx(i) + } + } else { + // tx spends B + Transaction { + input: vec![TxIn { + previous_output: op_b, + ..Default::default() + }], + ..new_tx(i) + } + }; + + let update = TxUpdate { + txs: vec![Arc::new(tx)], + ..Default::default() + }; + let _ = graph.apply_update_at(update, Some(i as u64)); } } + #[test] + fn test_add_conflicts() { + let (mut graph, chain) = init_graph_chain(); + let txouts: Vec<_> = graph.graph().all_txouts().collect(); + assert_eq!(txouts.len(), 1); + let prevout = txouts.first().unwrap().0; + add_conflicts(3, &mut graph, prevout); + + let unspent = graph.graph().filter_chain_unspents( + &chain, + chain.tip().block_id(), + graph.index.outpoints().clone(), + ); + assert_eq!(unspent.count(), 1); + } + + #[test] + fn test_chain_unconfirmed() { + let (mut graph, _) = init_graph_chain(); + let (prevout, _txout) = graph + .graph() + .all_txouts() + .find(|(_, txout)| txout.value == Amount::ONE_BTC) + .expect("initial graph should have txout"); + chain_unconfirmed(5, &mut graph, prevout); + assert_eq!(graph.graph().txs.len(), 6); // 1 onchain + 5 unconfirmed + } + + #[test] + #[rustfmt::skip] + fn test_nested() { + let (mut graph, chain) = init_graph_chain(); + let chain_tip = chain.tip().block_id(); + let n = 5; + add_nested_conflicts(n, &mut graph, &chain); + + let op = graph.index.outpoints().clone(); + assert_eq!(graph.graph().full_txs().count() as u32, 2 + n); // 2 onchain + n unconfirmed + assert_eq!(graph.graph().filter_chain_txouts(&chain, chain_tip, op).count(), 2); // 2 onchain + assert_eq!(graph.graph().list_canonical_txs(&chain, chain_tip).count(), 2 + 2); // 2 onchain + 2 unconfirmed + } +} + +/// Bench +#[allow(missing_docs)] +#[cfg(bdk_bench)] +pub mod bench { + use std::hint::black_box; + + use criterion::Criterion; + + use super::bench_util::*; + + #[inline(never)] pub fn filter_chain_unspents(bench: &mut Criterion) { - let (graph, chain) = get_params(); - // TODO: insert conflicts + let (mut graph, chain) = black_box(init_graph_chain()); + let prevout = graph.graph().all_txouts().next().unwrap().0; + black_box(add_conflicts(1000, &mut graph, prevout)); + bench.bench_function("filter_chain_unspents", |b| { b.iter(|| { - TxGraph::filter_chain_unspents( - graph.graph(), + let unspent = graph.graph().filter_chain_unspents( &chain, chain.tip().block_id(), graph.index.outpoints().clone(), - ) + ); + assert_eq!(unspent.count(), 1); + }) + }); + } + + #[inline(never)] + pub fn list_canonical_txs(bench: &mut Criterion) { + let (mut graph, chain) = black_box(init_graph_chain()); + let prevout = graph.graph().all_txouts().next().unwrap().0; + black_box(chain_unconfirmed(100, &mut graph, prevout)); + + bench.bench_function("list_canonical_txs", |b| { + b.iter(|| { + let txs = graph + .graph() + .list_canonical_txs(&chain, chain.tip().block_id()); + // 1 onchain + 100 unconfirmed + assert_eq!(txs.count(), 1 + 100); + }) + }); + } + + #[inline(never)] + pub fn nested_conflicts(bench: &mut Criterion) { + let (mut graph, chain) = black_box(init_graph_chain()); + black_box(add_nested_conflicts(2000, &mut graph, &chain)); + let graph = graph.graph(); + let chain_tip = chain.tip().block_id(); + + bench.bench_function("nested_conflicts", |b| { + b.iter(|| { + let txs = graph.list_canonical_txs(&chain, chain_tip); + // 2 onchain + 2 unconfirmed + assert_eq!(txs.count(), 4); }) }); }