Skip to content

Commit 5050537

Browse files
committed
Merge #1968: bench: Add reindex_tx_graph benchmark
f51f5b5 docs(chain): Improve API docs (valued mammal) fcf3834 test(keychain_txout): test spk cache (valued mammal) e07ec1c bench(chain): Add `reindex_tx_graph` benchmark (valued mammal) Pull request description: This PR contains the following changes: - Add benchmark `reindex_tx_graph`. Run with `cargo bench -p bdk_chain --bench indexer`. - Add unit test to `keychain_txout` module to test behavior of spk-cache. - Fixup a few doc comments. ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo +nightly fmt` and `cargo clippy` before committing ACKs for top commit: ValuedMammal: ACK f51f5b5 Tree-SHA512: 4d6826a825fa40fa13bd622ed6cff49366c541a7e0a7035a45fbeb98853d693ddef7fab113f6d77259fd75eddfc91623834363e028235fb26336c8865fccb02b
2 parents 45573ea + f51f5b5 commit 5050537

File tree

4 files changed

+199
-7
lines changed

4 files changed

+199
-7
lines changed

crates/chain/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,7 @@ rusqlite = ["std", "dep:rusqlite", "serde"]
4040
[[bench]]
4141
name = "canonicalization"
4242
harness = false
43+
44+
[[bench]]
45+
name = "indexer"
46+
harness = false

crates/chain/benches/indexer.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use bdk_chain::{
2+
keychain_txout::{InsertDescriptorError, KeychainTxOutIndex},
3+
local_chain::LocalChain,
4+
CanonicalizationParams, IndexedTxGraph,
5+
};
6+
use bdk_core::{BlockId, CheckPoint, ConfirmationBlockTime, TxUpdate};
7+
use bitcoin::{
8+
absolute, constants, hashes::Hash, key::Secp256k1, transaction, Amount, BlockHash, Network,
9+
Transaction, TxIn, TxOut,
10+
};
11+
use criterion::{black_box, criterion_group, criterion_main, Criterion};
12+
use miniscript::Descriptor;
13+
use std::sync::Arc;
14+
15+
type Keychain = ();
16+
type KeychainTxGraph = IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<Keychain>>;
17+
18+
const DESC: &str = "tr([ab28dc00/86h/1h/0h]tpubDCdDtzAMZZrkwKBxwNcGCqe4FRydeD9rfMisoi7qLdraG79YohRfPW4YgdKQhpgASdvh612xXNY5xYzoqnyCgPbkpK4LSVcH5Xv4cK7johH/0/*)";
19+
const LOOKAHEAD: u32 = 10;
20+
const LAST_REVEALED: u32 = 500;
21+
const TX_CT: u32 = 21;
22+
const USE_SPK_CACHE: bool = true;
23+
const AMOUNT: Amount = Amount::from_sat(1_000);
24+
25+
fn new_tx(lt: u32) -> Transaction {
26+
Transaction {
27+
version: transaction::Version::TWO,
28+
lock_time: absolute::LockTime::from_consensus(lt),
29+
input: vec![],
30+
output: vec![TxOut::NULL],
31+
}
32+
}
33+
34+
fn genesis_block_id() -> BlockId {
35+
BlockId {
36+
height: 0,
37+
hash: constants::genesis_block(Network::Regtest).block_hash(),
38+
}
39+
}
40+
41+
fn tip_block_id() -> BlockId {
42+
BlockId {
43+
height: 100,
44+
hash: BlockHash::all_zeros(),
45+
}
46+
}
47+
48+
fn setup<F: Fn(&mut KeychainTxGraph, &LocalChain)>(f: F) -> (KeychainTxGraph, LocalChain) {
49+
let desc = Descriptor::parse_descriptor(&Secp256k1::new(), DESC)
50+
.unwrap()
51+
.0;
52+
53+
let cp = CheckPoint::from_block_ids([genesis_block_id(), tip_block_id()]).unwrap();
54+
let chain = LocalChain::from_tip(cp).unwrap();
55+
56+
let mut index = KeychainTxOutIndex::new(LOOKAHEAD, USE_SPK_CACHE);
57+
index.insert_descriptor((), desc).unwrap();
58+
let mut tx_graph = KeychainTxGraph::new(index);
59+
60+
f(&mut tx_graph, &chain);
61+
62+
(tx_graph, chain)
63+
}
64+
65+
/// Bench performance of recovering `KeychainTxOutIndex` from changeset.
66+
fn do_bench(indexed_tx_graph: &KeychainTxGraph, chain: &LocalChain) {
67+
let desc = indexed_tx_graph.index.get_descriptor(()).unwrap();
68+
let changeset = indexed_tx_graph.initial_changeset();
69+
70+
// Now recover
71+
let (graph, _cs) =
72+
KeychainTxGraph::from_changeset(changeset, |cs| -> Result<_, InsertDescriptorError<_>> {
73+
let mut index = KeychainTxOutIndex::from_changeset(LOOKAHEAD, USE_SPK_CACHE, cs);
74+
let _ = index.insert_descriptor((), desc.clone())?;
75+
Ok(index)
76+
})
77+
.unwrap();
78+
79+
// Check balance
80+
let chain_tip = chain.tip().block_id();
81+
let op = graph.index.outpoints().clone();
82+
let bal = graph.graph().balance(
83+
chain,
84+
chain_tip,
85+
CanonicalizationParams::default(),
86+
op,
87+
|_, _| false,
88+
);
89+
assert_eq!(bal.total(), AMOUNT * TX_CT as u64);
90+
}
91+
92+
pub fn reindex_tx_graph(c: &mut Criterion) {
93+
let (graph, chain) = black_box(setup(|graph, _chain| {
94+
// Add relevant txs to graph
95+
for i in 0..TX_CT {
96+
let script_pubkey = graph.index.reveal_next_spk(()).unwrap().0 .1;
97+
let tx = Transaction {
98+
input: vec![TxIn::default()],
99+
output: vec![TxOut {
100+
script_pubkey,
101+
value: AMOUNT,
102+
}],
103+
..new_tx(i)
104+
};
105+
let txid = tx.compute_txid();
106+
let mut update = TxUpdate::default();
107+
update.seen_ats = [(txid, i as u64)].into();
108+
update.txs = vec![Arc::new(tx)];
109+
let _ = graph.apply_update(update);
110+
}
111+
// Reveal some SPKs
112+
let _ = graph.index.reveal_to_target((), LAST_REVEALED);
113+
}));
114+
115+
c.bench_function("reindex_tx_graph", {
116+
move |b| b.iter(|| do_bench(&graph, &chain))
117+
});
118+
}
119+
120+
criterion_group!(benches, reindex_tx_graph);
121+
criterion_main!(benches);

crates/chain/src/indexed_tx_graph.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ where
112112
///
113113
/// let (graph, reindex_cs) =
114114
/// IndexedTxGraph::from_changeset(persisted_changeset, move |idx_cs| -> anyhow::Result<_> {
115-
/// // e.g. KeychainTxOutIndex needs descriptors that weren’t in its CS
115+
/// // e.g. KeychainTxOutIndex needs descriptors that weren’t in its change set.
116116
/// let mut idx = KeychainTxOutIndex::from_changeset(DEFAULT_LOOKAHEAD, true, idx_cs);
117117
/// if let Some(desc) = persisted_desc {
118118
/// idx.insert_descriptor("external", desc)?;

crates/chain/src/indexer/keychain_txout.rs

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
200200
}
201201

202202
impl<K> KeychainTxOutIndex<K> {
203-
/// Construct a [`KeychainTxOutIndex`] with the given `lookahead` and `use_spk_cache` boolean.
203+
/// Construct a [`KeychainTxOutIndex`] with the given `lookahead` and `persist_spks` boolean.
204204
///
205205
/// # Lookahead
206206
///
@@ -221,10 +221,10 @@ impl<K> KeychainTxOutIndex<K> {
221221
///
222222
/// ```rust
223223
/// # use bdk_chain::keychain_txout::KeychainTxOutIndex;
224-
/// // Derive 20 future addresses per chain and persist + reload script pubkeys via ChangeSets:
224+
/// // Derive 20 future addresses per keychain and persist + reload script pubkeys via ChangeSets:
225225
/// let idx = KeychainTxOutIndex::<&'static str>::new(20, true);
226226
///
227-
/// // Derive 10 future addresses per chain without persistence:
227+
/// // Derive 10 future addresses per keychain without persistence:
228228
/// let idx = KeychainTxOutIndex::<&'static str>::new(10, false);
229229
/// ```
230230
pub fn new(lookahead: u32, persist_spks: bool) -> Self {
@@ -251,7 +251,7 @@ impl<K> KeychainTxOutIndex<K> {
251251
impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
252252
/// Construct `KeychainTxOutIndex<K>` from the given `changeset`.
253253
///
254-
/// Shorthand for called [`new`] and then [`apply_changeset`].
254+
/// Shorthand for calling [`new`] and then [`apply_changeset`].
255255
///
256256
/// [`new`]: Self::new
257257
/// [`apply_changeset`]: Self::apply_changeset
@@ -1002,7 +1002,7 @@ impl<K: core::fmt::Debug> std::error::Error for InsertDescriptorError<K> {}
10021002
///
10031003
/// It tracks:
10041004
/// 1. `last_revealed`: the highest derivation index revealed per descriptor.
1005-
/// 2. `spks`: the cache of derived script pubkeys to persist across runs.
1005+
/// 2. `spk_cache`: the cache of derived script pubkeys to persist across runs.
10061006
///
10071007
/// You can apply a `ChangeSet` to a `KeychainTxOutIndex` via
10081008
/// [`KeychainTxOutIndex::apply_changeset`], or merge two change sets with [`ChangeSet::merge`].
@@ -1011,7 +1011,7 @@ impl<K: core::fmt::Debug> std::error::Error for InsertDescriptorError<K> {}
10111011
///
10121012
/// - `last_revealed` is monotonic: merging retains the maximum index for each descriptor and never
10131013
/// decreases.
1014-
/// - `spks` accumulates entries: once a script pubkey is persisted, it remains available for
1014+
/// - `spk_cache` accumulates entries: once a script pubkey is persisted, it remains available for
10151015
/// reload. If the same descriptor and index appear again with a new script pubkey, the latter
10161016
/// value overrides the former.
10171017
///
@@ -1107,3 +1107,70 @@ impl<K: Clone + Ord + core::fmt::Debug> FullScanRequestBuilderExt<K> for FullSca
11071107
self
11081108
}
11091109
}
1110+
1111+
#[cfg(test)]
1112+
mod test {
1113+
use super::*;
1114+
1115+
use bdk_testenv::utils::DESCRIPTORS;
1116+
use bitcoin::secp256k1::Secp256k1;
1117+
use miniscript::Descriptor;
1118+
1119+
// Test that `KeychainTxOutIndex` uses the spk cache.
1120+
// And the indexed spks are as expected.
1121+
#[test]
1122+
fn test_spk_cache() {
1123+
let lookahead = 10;
1124+
let use_cache = true;
1125+
let mut index = KeychainTxOutIndex::new(lookahead, use_cache);
1126+
let s = DESCRIPTORS[0];
1127+
1128+
let desc = Descriptor::parse_descriptor(&Secp256k1::new(), s)
1129+
.unwrap()
1130+
.0;
1131+
1132+
let did = desc.descriptor_id();
1133+
1134+
let reveal_to = 2;
1135+
let end_index = reveal_to + lookahead;
1136+
1137+
let _ = index.insert_descriptor(0i32, desc.clone());
1138+
assert_eq!(index.spk_cache.get(&did).unwrap().len() as u32, lookahead);
1139+
assert_eq!(index.next_index(0), Some((0, true)));
1140+
1141+
// Now reveal some scripts
1142+
for _ in 0..=reveal_to {
1143+
let _ = index.reveal_next_spk(0).unwrap();
1144+
}
1145+
assert_eq!(index.last_revealed_index(0), Some(reveal_to));
1146+
1147+
let spk_cache = &index.spk_cache;
1148+
assert!(!spk_cache.is_empty());
1149+
1150+
for (&did, cached_spks) in spk_cache {
1151+
assert_eq!(did, desc.descriptor_id());
1152+
for (&i, cached_spk) in cached_spks {
1153+
// Cached spk matches derived
1154+
let exp_spk = desc.at_derivation_index(i).unwrap().script_pubkey();
1155+
assert_eq!(&exp_spk, cached_spk);
1156+
// Also matches the inner index
1157+
assert_eq!(index.spk_at_index(0, i), Some(cached_spk.clone()));
1158+
}
1159+
}
1160+
1161+
let init_cs = index.initial_changeset();
1162+
assert_eq!(
1163+
init_cs.spk_cache.get(&did).unwrap().len() as u32,
1164+
end_index + 1
1165+
);
1166+
1167+
// Now test load from changeset
1168+
let recovered =
1169+
KeychainTxOutIndex::<&str>::from_changeset(lookahead, use_cache, init_cs.clone());
1170+
assert_eq!(&recovered.spk_cache, spk_cache);
1171+
1172+
// The cache is optional at load time
1173+
let index = KeychainTxOutIndex::<i32>::from_changeset(lookahead, false, init_cs);
1174+
assert!(index.spk_cache.is_empty());
1175+
}
1176+
}

0 commit comments

Comments
 (0)