Skip to content

Commit 4b39650

Browse files
perf(chain): add benchmarks for canonicalization logic
This is mostly taken from bitcoindevkit#1735 except we inline many of the functions and test `list_canonical_txs`, `filter_chain_unspents` and `filter_chain_txouts` on all scenarios. Co-authored-by: valued mammal <[email protected]>
1 parent d7f7bd5 commit 4b39650

File tree

2 files changed

+259
-1
lines changed

2 files changed

+259
-1
lines changed

crates/chain/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,15 @@ serde_json = { version = "1", optional = true }
2929
rand = "0.8"
3030
proptest = "1.2.0"
3131
bdk_testenv = { path = "../testenv", default-features = false }
32-
32+
criterion = { version = "0.5", features = ["html_reports"] }
3333

3434
[features]
3535
default = ["std", "miniscript"]
3636
std = ["bitcoin/std", "miniscript?/std", "bdk_core/std"]
3737
serde = ["dep:serde", "bitcoin/serde", "miniscript?/serde", "bdk_core/serde"]
3838
hashbrown = ["bdk_core/hashbrown"]
3939
rusqlite = ["std", "dep:rusqlite", "serde", "serde_json"]
40+
41+
[[bench]]
42+
name = "canonicalization"
43+
harness = false
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
use bdk_chain::{keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, IndexedTxGraph};
2+
use bdk_core::{BlockId, CheckPoint};
3+
use bdk_core::{ConfirmationBlockTime, TxUpdate};
4+
use bdk_testenv::hash;
5+
use bitcoin::{
6+
absolute, constants, hashes::Hash, key::Secp256k1, transaction, Amount, BlockHash, Network,
7+
OutPoint, ScriptBuf, Transaction, TxIn, TxOut,
8+
};
9+
use criterion::{black_box, criterion_group, criterion_main, Criterion};
10+
use miniscript::{Descriptor, DescriptorPublicKey};
11+
use std::sync::Arc;
12+
13+
type Keychain = ();
14+
type KeychainTxGraph = IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<Keychain>>;
15+
16+
/// New tx guaranteed to have at least one output
17+
fn new_tx(lt: u32) -> Transaction {
18+
Transaction {
19+
version: transaction::Version::TWO,
20+
lock_time: absolute::LockTime::from_consensus(lt),
21+
input: vec![],
22+
output: vec![TxOut::NULL],
23+
}
24+
}
25+
26+
fn spk_at_index(txout_index: &KeychainTxOutIndex<Keychain>, index: u32) -> ScriptBuf {
27+
txout_index
28+
.get_descriptor(())
29+
.unwrap()
30+
.at_derivation_index(index)
31+
.unwrap()
32+
.script_pubkey()
33+
}
34+
35+
fn genesis_block_id() -> BlockId {
36+
BlockId {
37+
height: 0,
38+
hash: constants::genesis_block(Network::Regtest).block_hash(),
39+
}
40+
}
41+
42+
fn tip_block_id() -> BlockId {
43+
BlockId {
44+
height: 100,
45+
hash: BlockHash::all_zeros(),
46+
}
47+
}
48+
49+
/// Add ancestor tx confirmed at `block_id` with `locktime` (used for uniqueness).
50+
/// The transaction always pays 1 BTC to SPK 0.
51+
fn add_ancestor_tx(graph: &mut KeychainTxGraph, block_id: BlockId, locktime: u32) -> OutPoint {
52+
let spk_0 = spk_at_index(&graph.index, 0);
53+
let tx = Transaction {
54+
input: vec![TxIn {
55+
previous_output: OutPoint::new(hash!("bogus"), locktime),
56+
..Default::default()
57+
}],
58+
output: vec![TxOut {
59+
value: Amount::ONE_BTC,
60+
script_pubkey: spk_0,
61+
}],
62+
..new_tx(locktime)
63+
};
64+
let txid = tx.compute_txid();
65+
let _ = graph.insert_tx(tx);
66+
let _ = graph.insert_anchor(
67+
txid,
68+
ConfirmationBlockTime {
69+
block_id,
70+
confirmation_time: 100,
71+
},
72+
);
73+
OutPoint { txid, vout: 0 }
74+
}
75+
76+
fn setup<F: Fn(&mut KeychainTxGraph, &LocalChain)>(f: F) -> (KeychainTxGraph, LocalChain) {
77+
const DESC: &str = "tr([ab28dc00/86h/1h/0h]tpubDCdDtzAMZZrkwKBxwNcGCqe4FRydeD9rfMisoi7qLdraG79YohRfPW4YgdKQhpgASdvh612xXNY5xYzoqnyCgPbkpK4LSVcH5Xv4cK7johH/0/*)";
78+
let cp = CheckPoint::from_block_ids([genesis_block_id(), tip_block_id()])
79+
.expect("blocks must be chronological");
80+
let chain = LocalChain::from_tip(cp).unwrap();
81+
82+
let (desc, _) =
83+
<Descriptor<DescriptorPublicKey>>::parse_descriptor(&Secp256k1::new(), DESC).unwrap();
84+
let mut index = KeychainTxOutIndex::new(10);
85+
index.insert_descriptor((), desc).unwrap();
86+
let mut tx_graph = KeychainTxGraph::new(index);
87+
88+
f(&mut tx_graph, &chain);
89+
(tx_graph, chain)
90+
}
91+
92+
fn run_list_canonical_txs(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txs: usize) {
93+
let txs = tx_graph
94+
.graph()
95+
.list_canonical_txs(chain, chain.tip().block_id());
96+
assert_eq!(txs.count(), exp_txs);
97+
}
98+
99+
fn run_filter_chain_txouts(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_txos: usize) {
100+
let utxos = tx_graph.graph().filter_chain_txouts(
101+
chain,
102+
chain.tip().block_id(),
103+
tx_graph.index.outpoints().clone(),
104+
);
105+
assert_eq!(utxos.count(), exp_txos);
106+
}
107+
108+
fn run_filter_chain_unspents(tx_graph: &KeychainTxGraph, chain: &LocalChain, exp_utxos: usize) {
109+
let utxos = tx_graph.graph().filter_chain_unspents(
110+
chain,
111+
chain.tip().block_id(),
112+
tx_graph.index.outpoints().clone(),
113+
);
114+
assert_eq!(utxos.count(), exp_utxos);
115+
}
116+
117+
pub fn many_conflicting_unconfirmed(c: &mut Criterion) {
118+
const CONFLICTING_TX_COUNT: u32 = 2100;
119+
let (tx_graph, chain) = black_box(setup(|tx_graph, _chain| {
120+
let previous_output = add_ancestor_tx(tx_graph, tip_block_id(), 0);
121+
// Create conflicting txs that spend from `previous_output`.
122+
let spk_1 = spk_at_index(&tx_graph.index, 1);
123+
for i in 1..=CONFLICTING_TX_COUNT {
124+
let tx = Transaction {
125+
input: vec![TxIn {
126+
previous_output,
127+
..Default::default()
128+
}],
129+
output: vec![TxOut {
130+
value: Amount::ONE_BTC - Amount::from_sat(i as u64 * 10),
131+
script_pubkey: spk_1.clone(),
132+
}],
133+
..new_tx(i)
134+
};
135+
let update = TxUpdate {
136+
txs: vec![Arc::new(tx)],
137+
..Default::default()
138+
};
139+
let _ = tx_graph.apply_update_at(update, Some(i as u64));
140+
}
141+
}));
142+
c.bench_function("many_conflicting_unconfirmed::list_canonical_txs", |b| {
143+
b.iter(|| run_list_canonical_txs(&tx_graph, &chain, 2))
144+
});
145+
c.bench_function("many_conflicting_unconfirmed::filter_chain_txouts", |b| {
146+
b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, 2))
147+
});
148+
c.bench_function("many_conflicting_unconfirmed::filter_chain_unspents", |b| {
149+
b.iter(|| run_filter_chain_unspents(&tx_graph, &chain, 1))
150+
});
151+
}
152+
153+
pub fn many_chained_unconfirmed(c: &mut Criterion) {
154+
const TX_CHAIN_COUNT: u32 = 2100;
155+
let (tx_graph, chain) = black_box(setup(|tx_graph, _chain| {
156+
let mut previous_output = add_ancestor_tx(tx_graph, tip_block_id(), 0);
157+
// Create a chain of unconfirmed txs where each subsequent tx spends the output of the
158+
// previous one.
159+
for i in 0..TX_CHAIN_COUNT {
160+
// Create tx.
161+
let tx = Transaction {
162+
input: vec![TxIn {
163+
previous_output,
164+
..Default::default()
165+
}],
166+
..new_tx(i)
167+
};
168+
let txid = tx.compute_txid();
169+
let update = TxUpdate {
170+
txs: vec![Arc::new(tx)],
171+
..Default::default()
172+
};
173+
let _ = tx_graph.apply_update_at(update, Some(i as u64));
174+
// Store the next prevout.
175+
previous_output = OutPoint::new(txid, 0);
176+
}
177+
}));
178+
c.bench_function("many_chained_unconfirmed::list_canonical_txs", |b| {
179+
b.iter(|| run_list_canonical_txs(&tx_graph, &chain, 2101))
180+
});
181+
c.bench_function("many_chained_unconfirmed::filter_chain_txouts", |b| {
182+
b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, 1))
183+
});
184+
c.bench_function("many_chained_unconfirmed::filter_chain_unspents", |b| {
185+
b.iter(|| run_filter_chain_unspents(&tx_graph, &chain, 0))
186+
});
187+
}
188+
189+
pub fn nested_conflicts(c: &mut Criterion) {
190+
const TX_COUNT: u32 = 2100;
191+
let (tx_graph, chain) = black_box(setup(|tx_graph, _chain| {
192+
let op_a = add_ancestor_tx(tx_graph, tip_block_id(), 0);
193+
let op_b = add_ancestor_tx(tx_graph, tip_block_id(), 1);
194+
for i in 0..TX_COUNT {
195+
let tx = if i == TX_COUNT / 2 {
196+
// tx spends both A, B
197+
Transaction {
198+
input: vec![
199+
TxIn {
200+
previous_output: op_a,
201+
..Default::default()
202+
},
203+
TxIn {
204+
previous_output: op_b,
205+
..Default::default()
206+
},
207+
],
208+
..new_tx(i)
209+
}
210+
} else if i % 2 == 1 {
211+
// tx spends A
212+
Transaction {
213+
input: vec![TxIn {
214+
previous_output: op_a,
215+
..Default::default()
216+
}],
217+
..new_tx(i)
218+
}
219+
} else {
220+
// tx spends B
221+
Transaction {
222+
input: vec![TxIn {
223+
previous_output: op_b,
224+
..Default::default()
225+
}],
226+
..new_tx(i)
227+
}
228+
};
229+
230+
let update = TxUpdate {
231+
txs: vec![Arc::new(tx)],
232+
..Default::default()
233+
};
234+
let _ = tx_graph.apply_update_at(update, Some(i as u64));
235+
}
236+
}));
237+
c.bench_function("nested_conflicts::list_canonical_txs", |b| {
238+
b.iter(|| run_list_canonical_txs(&tx_graph, &chain, 4))
239+
});
240+
c.bench_function("nested_conflicts::filter_chain_txouts", |b| {
241+
b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, 2))
242+
});
243+
c.bench_function("nested_conflicts::filter_chain_unspents", |b| {
244+
b.iter(|| run_filter_chain_unspents(&tx_graph, &chain, 0))
245+
});
246+
}
247+
248+
criterion_group!(
249+
benches,
250+
many_conflicting_unconfirmed,
251+
many_chained_unconfirmed,
252+
nested_conflicts,
253+
);
254+
criterion_main!(benches);

0 commit comments

Comments
 (0)