Skip to content

Commit 87267a4

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. CI and README is updated to pin `csv`. Co-authored-by: valued mammal <[email protected]>
1 parent d4bfb78 commit 87267a4

File tree

4 files changed

+259
-1
lines changed

4 files changed

+259
-1
lines changed

.github/workflows/cont_integration.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ jobs:
5151
cargo update -p tokio-util --precise "0.7.11"
5252
cargo update -p indexmap --precise "2.5.0"
5353
cargo update -p security-framework-sys --precise "2.11.1"
54+
cargo update -p csv --precise "1.3.0"
55+
cargo update -p unicode-width --precise "0.1.13"
5456
- name: Build
5557
run: cargo build --workspace --exclude 'example_*' ${{ matrix.features }}
5658
- name: Test

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ cargo update -p tokio --precise "1.38.1"
7878
cargo update -p tokio-util --precise "0.7.11"
7979
cargo update -p indexmap --precise "2.5.0"
8080
cargo update -p security-framework-sys --precise "2.11.1"
81+
cargo update -p csv --precise "1.3.0"
82+
cargo update -p unicode-width --precise "0.1.13"
8183
```
8284

8385
## License

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.2" }
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: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
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", {
143+
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
144+
move |b| b.iter(|| run_list_canonical_txs(&tx_graph, &chain, 2))
145+
});
146+
c.bench_function("many_conflicting_unconfirmed::filter_chain_txouts", {
147+
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
148+
move |b| b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, 2))
149+
});
150+
c.bench_function("many_conflicting_unconfirmed::filter_chain_unspents", {
151+
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
152+
move |b| b.iter(|| run_filter_chain_unspents(&tx_graph, &chain, 1))
153+
});
154+
}
155+
156+
pub fn many_chained_unconfirmed(c: &mut Criterion) {
157+
const TX_CHAIN_COUNT: u32 = 2100;
158+
let (tx_graph, chain) = black_box(setup(|tx_graph, _chain| {
159+
let mut previous_output = add_ancestor_tx(tx_graph, tip_block_id(), 0);
160+
// Create a chain of unconfirmed txs where each subsequent tx spends the output of the
161+
// previous one.
162+
for i in 0..TX_CHAIN_COUNT {
163+
// Create tx.
164+
let tx = Transaction {
165+
input: vec![TxIn {
166+
previous_output,
167+
..Default::default()
168+
}],
169+
..new_tx(i)
170+
};
171+
let txid = tx.compute_txid();
172+
let update = TxUpdate {
173+
txs: vec![Arc::new(tx)],
174+
..Default::default()
175+
};
176+
let _ = tx_graph.apply_update_at(update, Some(i as u64));
177+
// Store the next prevout.
178+
previous_output = OutPoint::new(txid, 0);
179+
}
180+
}));
181+
c.bench_function("many_chained_unconfirmed::list_canonical_txs", {
182+
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
183+
move |b| b.iter(|| run_list_canonical_txs(&tx_graph, &chain, 2101))
184+
});
185+
c.bench_function("many_chained_unconfirmed::filter_chain_txouts", {
186+
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
187+
move |b| b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, 1))
188+
});
189+
c.bench_function("many_chained_unconfirmed::filter_chain_unspents", {
190+
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
191+
move |b| b.iter(|| run_filter_chain_unspents(&tx_graph, &chain, 0))
192+
});
193+
}
194+
195+
pub fn nested_conflicts(c: &mut Criterion) {
196+
const CONFLICTS_PER_OUTPUT: usize = 3;
197+
const GRAPH_DEPTH: usize = 7;
198+
let (tx_graph, chain) = black_box(setup(|tx_graph, _chain| {
199+
let mut prev_ops = core::iter::once(add_ancestor_tx(tx_graph, tip_block_id(), 0))
200+
.collect::<Vec<OutPoint>>();
201+
for depth in 1..GRAPH_DEPTH {
202+
for previous_output in core::mem::take(&mut prev_ops) {
203+
for conflict_i in 1..=CONFLICTS_PER_OUTPUT {
204+
let mut last_seen = depth * conflict_i;
205+
if last_seen % 2 == 0 {
206+
last_seen /= 2;
207+
}
208+
let ((_, script_pubkey), _) = tx_graph.index.next_unused_spk(()).unwrap();
209+
let value =
210+
Amount::ONE_BTC - Amount::from_sat(depth as u64 * 200 - conflict_i as u64);
211+
let tx = Transaction {
212+
input: vec![TxIn {
213+
previous_output,
214+
..Default::default()
215+
}],
216+
output: vec![TxOut {
217+
value,
218+
script_pubkey,
219+
}],
220+
..new_tx(conflict_i as _)
221+
};
222+
let txid = tx.compute_txid();
223+
prev_ops.push(OutPoint::new(txid, 0));
224+
let _ = tx_graph.insert_seen_at(txid, last_seen as _);
225+
let _ = tx_graph.insert_tx(tx);
226+
}
227+
}
228+
}
229+
}));
230+
c.bench_function("nested_conflicts_unconfirmed::list_canonical_txs", {
231+
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
232+
move |b| b.iter(|| run_list_canonical_txs(&tx_graph, &chain, GRAPH_DEPTH))
233+
});
234+
c.bench_function("nested_conflicts_unconfirmed::filter_chain_txouts", {
235+
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
236+
move |b| b.iter(|| run_filter_chain_txouts(&tx_graph, &chain, GRAPH_DEPTH))
237+
});
238+
c.bench_function("nested_conflicts_unconfirmed::filter_chain_unspents", {
239+
let (tx_graph, chain) = (tx_graph.clone(), chain.clone());
240+
move |b| b.iter(|| run_filter_chain_unspents(&tx_graph, &chain, 1))
241+
});
242+
}
243+
244+
criterion_group!(
245+
benches,
246+
// many_conflicting_unconfirmed,
247+
// many_chained_unconfirmed,
248+
nested_conflicts,
249+
);
250+
criterion_main!(benches);

0 commit comments

Comments
 (0)