Skip to content

Commit 3d619c9

Browse files
committed
Merge #247: deps: Update bdk_chain to 0.23.0
6aba330 fix: gracefully handle `reveal_to_target` (valued mammal) 741f2ae test: persist indexed script pubkeys (valued mammal) db25555 deps!: Update `bdk_chain` to 0.23.0 (valued mammal) Pull request description: ### Description The PR updates bdk_chain to 0.23.0. Additionally we introduce the ability to persist derived SPKs indexed by descriptor ID and derivation index. The `CreateParams::use_spk_cache` method enables or disables a persistent cache for script pubkeys (SPKs) derived by the wallet. When enabled, the wallet will store and reuse previously derived SPKs, avoiding redundant derivation on subsequent loads. When a wallet has many revealed addresses (i.e., many derived SPKs), loading the wallet can become slow if every address must be re-derived from scratch. By enabling the SPK cache: - **On wallet creation:** The wallet will persistently store each derived SPK as addresses are revealed. - **On wallet load:** If `use_spk_cache` is also set in the corresponding `LoadParams`, the wallet will load the cached SPKs directly from storage, skipping the need to re-derive them from the descriptor. This can dramatically reduce load times for wallets with hundreds or thousands of revealed addresses, as the expensive crypto operations are avoided. **Caveat:** Both creation and loading must have `use_spk_cache(true)` set for the cache to be used. If not, the wallet will fall back to deriving SPKs as usual. fixes #246 fixes #237 ### Changelog notice #### Added - `CreateParams::use_spk_cache` - `LoadParams::use_spk_cache` #### Changed - bump bdk_chain to 0.23.0 - **Note:** This change extends the wallet `ChangeSet` type by adding `first_seen` to the tx_graph member, and adding `spk_cache` to the indexer. ### 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 * [x] I've added tests for the new feature * [x] I've added docs for the new feature * [x] This pull request breaks the existing API * [x] I'm linking the issue being fixed by this PR ACKs for top commit: notmandatory: ACK 6aba330 Tree-SHA512: dd4d657e61836d12da388f233dcf463638da457fd1c982d13ee469dfdf8df77f3351a6edaa300156755217427da8296db03d2161c7baf3cb71cf5626b355680a
2 parents 151e49e + 6aba330 commit 3d619c9

File tree

10 files changed

+139
-38
lines changed

10 files changed

+139
-38
lines changed

examples/example_wallet_electrum/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ edition = "2021"
55

66
[dependencies]
77
bdk_wallet = { path = "../../wallet", features = ["file_store"] }
8-
bdk_electrum = { version = "0.22.0" }
8+
bdk_electrum = { version = "0.23.0" }
99
anyhow = "1"

examples/example_wallet_esplora_async/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ edition = "2021"
77

88
[dependencies]
99
bdk_wallet = { path = "../../wallet", features = ["rusqlite"] }
10-
bdk_esplora = { version = "0.21.0", features = ["async-https", "tokio"] }
10+
bdk_esplora = { version = "0.22.0", features = ["async-https", "tokio"] }
1111
tokio = { version = "1.38.1", features = ["rt", "rt-multi-thread", "macros"] }
1212
anyhow = "1"

examples/example_wallet_esplora_blocking/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ publish = false
88

99
[dependencies]
1010
bdk_wallet = { path = "../../wallet", features = ["file_store"] }
11-
bdk_esplora = { version = "0.21.0", features = ["blocking"] }
11+
bdk_esplora = { version = "0.22.0", features = ["blocking"] }
1212
anyhow = "1"

examples/example_wallet_rpc/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ edition = "2021"
77

88
[dependencies]
99
bdk_wallet = { path = "../../wallet", features = ["file_store"] }
10-
bdk_bitcoind_rpc = { version = "0.19.0" }
10+
bdk_bitcoind_rpc = { version = "0.20.0" }
1111

1212
anyhow = "1"
1313
clap = { version = "4.5.17", features = ["derive", "env"] }

wallet/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ miniscript = { version = "12.3.1", features = [ "serde" ], default-features = fa
2121
bitcoin = { version = "0.32.4", features = [ "serde", "base64" ], default-features = false }
2222
serde = { version = "^1.0", features = ["derive"] }
2323
serde_json = { version = "^1.0" }
24-
bdk_chain = { version = "0.22.0", features = [ "miniscript", "serde" ], default-features = false }
24+
bdk_chain = { version = "0.23.0", features = [ "miniscript", "serde" ], default-features = false }
2525

2626
# Optional dependencies
2727
bip39 = { version = "2.0", optional = true }
28-
bdk_file_store = { version = "0.20.0", optional = true }
28+
bdk_file_store = { version = "0.21.0", optional = true }
2929

3030
[features]
3131
default = ["std"]
@@ -40,7 +40,7 @@ test-utils = ["std"]
4040
[dev-dependencies]
4141
assert_matches = "1.5.0"
4242
tempfile = "3"
43-
bdk_chain = { version = "0.22.0", features = ["rusqlite"] }
43+
bdk_chain = { version = "0.23.0", features = ["rusqlite"] }
4444
bdk_wallet = { path = ".", features = ["rusqlite", "file_store", "test-utils"] }
4545
anyhow = "1"
4646
rand = "^0.8"

wallet/src/wallet/coin_selection.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,7 @@ mod test {
750750
value,
751751
index,
752752
ChainPosition::Unconfirmed {
753+
first_seen: Some(last_seen),
753754
last_seen: Some(last_seen),
754755
},
755756
)
@@ -850,7 +851,10 @@ mod test {
850851
transitively: None,
851852
}
852853
} else {
853-
ChainPosition::Unconfirmed { last_seen: Some(0) }
854+
ChainPosition::Unconfirmed {
855+
first_seen: Some(1),
856+
last_seen: Some(1),
857+
}
854858
},
855859
}),
856860
});
@@ -875,7 +879,10 @@ mod test {
875879
keychain: KeychainKind::External,
876880
is_spent: false,
877881
derivation_index: 42,
878-
chain_position: ChainPosition::Unconfirmed { last_seen: Some(0) },
882+
chain_position: ChainPosition::Unconfirmed {
883+
first_seen: Some(1),
884+
last_seen: Some(1),
885+
},
879886
}),
880887
})
881888
.collect()
@@ -1231,7 +1238,10 @@ mod test {
12311238
optional.push(utxo(
12321239
Amount::from_sat(500_000),
12331240
3,
1234-
ChainPosition::<ConfirmationBlockTime>::Unconfirmed { last_seen: Some(0) },
1241+
ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
1242+
first_seen: Some(1),
1243+
last_seen: Some(1),
1244+
},
12351245
));
12361246

12371247
// Defensive assertions, for sanity and in case someone changes the test utxos vector.

wallet/src/wallet/mod.rs

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,12 @@ impl Wallet {
438438
None => (None, Arc::new(SignersContainer::new())),
439439
};
440440

441-
let index = create_indexer(descriptor, change_descriptor, params.lookahead)?;
441+
let index = create_indexer(
442+
descriptor,
443+
change_descriptor,
444+
params.lookahead,
445+
params.use_spk_cache,
446+
)?;
442447

443448
let descriptor = index.get_descriptor(KeychainKind::External).cloned();
444449
let change_descriptor = index.get_descriptor(KeychainKind::Internal).cloned();
@@ -637,8 +642,13 @@ impl Wallet {
637642
None => Arc::new(SignersContainer::new()),
638643
};
639644

640-
let index = create_indexer(descriptor, change_descriptor, params.lookahead)
641-
.map_err(LoadError::Descriptor)?;
645+
let index = create_indexer(
646+
descriptor,
647+
change_descriptor,
648+
params.lookahead,
649+
params.use_spk_cache,
650+
)
651+
.map_err(LoadError::Descriptor)?;
642652

643653
let mut indexed_graph = IndexedTxGraph::new(index);
644654
indexed_graph.apply_changeset(changeset.indexer.into());
@@ -1118,9 +1128,9 @@ impl Wallet {
11181128
/// "tx is an ancestor of a tx anchored in {}:{}",
11191129
/// anchor.block_id.height, anchor.block_id.hash,
11201130
/// ),
1121-
/// ChainPosition::Unconfirmed { last_seen } => println!(
1122-
/// "tx is last seen at {:?}, it is unconfirmed as it is not anchored in the best chain",
1123-
/// last_seen,
1131+
/// ChainPosition::Unconfirmed { first_seen, last_seen } => println!(
1132+
/// "tx is first seen at {:?}, last seen at {:?}, it is unconfirmed as it is not anchored in the best chain",
1133+
/// first_seen, last_seen
11241134
/// ),
11251135
/// }
11261136
/// ```
@@ -1611,13 +1621,12 @@ impl Wallet {
16111621

16121622
// recording changes to the change keychain
16131623
if let (Excess::Change { .. }, Some((keychain, index))) = (excess, drain_index) {
1614-
let (_, index_changeset) = self
1615-
.indexed_graph
1616-
.index
1617-
.reveal_to_target(keychain, index)
1618-
.expect("must not be None");
1619-
self.stage.merge(index_changeset.into());
1620-
self.mark_used(keychain, index);
1624+
if let Some((_, index_changeset)) =
1625+
self.indexed_graph.index.reveal_to_target(keychain, index)
1626+
{
1627+
self.stage.merge(index_changeset.into());
1628+
self.mark_used(keychain, index);
1629+
}
16211630
}
16221631

16231632
Ok(psbt)
@@ -2619,17 +2628,14 @@ fn create_indexer(
26192628
descriptor: ExtendedDescriptor,
26202629
change_descriptor: Option<ExtendedDescriptor>,
26212630
lookahead: u32,
2631+
use_spk_cache: bool,
26222632
) -> Result<KeychainTxOutIndex<KeychainKind>, DescriptorError> {
2623-
let mut indexer = KeychainTxOutIndex::<KeychainKind>::new(lookahead);
2633+
let mut indexer = KeychainTxOutIndex::<KeychainKind>::new(lookahead, use_spk_cache);
26242634

2625-
// let (descriptor, keymap) = descriptor;
2626-
// let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
26272635
assert!(indexer
26282636
.insert_descriptor(KeychainKind::External, descriptor)
26292637
.expect("first descriptor introduced must succeed"));
26302638

2631-
// let (descriptor, keymap) = change_descriptor;
2632-
// let change_signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
26332639
if let Some(change_descriptor) = change_descriptor {
26342640
assert!(indexer
26352641
.insert_descriptor(KeychainKind::Internal, change_descriptor)

wallet/src/wallet/params.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub struct CreateParams {
3939
pub(crate) network: Network,
4040
pub(crate) genesis_hash: Option<BlockHash>,
4141
pub(crate) lookahead: u32,
42+
pub(crate) use_spk_cache: bool,
4243
}
4344

4445
impl CreateParams {
@@ -61,6 +62,7 @@ impl CreateParams {
6162
network: Network::Bitcoin,
6263
genesis_hash: None,
6364
lookahead: DEFAULT_LOOKAHEAD,
65+
use_spk_cache: false,
6466
}
6567
}
6668

@@ -82,6 +84,7 @@ impl CreateParams {
8284
network: Network::Bitcoin,
8385
genesis_hash: None,
8486
lookahead: DEFAULT_LOOKAHEAD,
87+
use_spk_cache: false,
8588
}
8689
}
8790

@@ -118,6 +121,15 @@ impl CreateParams {
118121
self
119122
}
120123

124+
/// Use a persistent cache of indexed script pubkeys (SPKs).
125+
///
126+
/// **Note:** To persist across restarts, this option must also be set at load time with
127+
/// [`LoadParams`](LoadParams::use_spk_cache).
128+
pub fn use_spk_cache(mut self, use_spk_cache: bool) -> Self {
129+
self.use_spk_cache = use_spk_cache;
130+
self
131+
}
132+
121133
/// Create [`PersistedWallet`] with the given [`WalletPersister`].
122134
pub fn create_wallet<P>(
123135
self,
@@ -157,6 +169,7 @@ pub struct LoadParams {
157169
pub(crate) check_descriptor: Option<Option<DescriptorToExtract>>,
158170
pub(crate) check_change_descriptor: Option<Option<DescriptorToExtract>>,
159171
pub(crate) extract_keys: bool,
172+
pub(crate) use_spk_cache: bool,
160173
}
161174

162175
impl LoadParams {
@@ -173,6 +186,7 @@ impl LoadParams {
173186
check_descriptor: None,
174187
check_change_descriptor: None,
175188
extract_keys: false,
189+
use_spk_cache: false,
176190
}
177191
}
178192

@@ -234,6 +248,15 @@ impl LoadParams {
234248
self
235249
}
236250

251+
/// Use a persistent cache of indexed script pubkeys (SPKs).
252+
///
253+
/// **Note:** This should only be used if you have previously persisted a cache of script
254+
/// pubkeys using [`CreateParams::use_spk_cache`].
255+
pub fn use_spk_cache(mut self, use_spk_cache: bool) -> Self {
256+
self.use_spk_cache = use_spk_cache;
257+
self
258+
}
259+
237260
/// Load [`PersistedWallet`] with the given [`WalletPersister`].
238261
pub fn load_wallet<P>(
239262
self,

wallet/src/wallet/tx_builder.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1024,7 +1024,10 @@ mod test {
10241024
txout: TxOut::NULL,
10251025
keychain: KeychainKind::External,
10261026
is_spent: false,
1027-
chain_position: chain::ChainPosition::Unconfirmed { last_seen: Some(0) },
1027+
chain_position: chain::ChainPosition::Unconfirmed {
1028+
first_seen: Some(1),
1029+
last_seen: Some(1),
1030+
},
10281031
derivation_index: 0,
10291032
},
10301033
LocalOutput {

wallet/tests/wallet.rs

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
use std::collections::BTreeMap;
12
use std::path::Path;
23
use std::str::FromStr;
34
use std::sync::Arc;
45

56
use anyhow::Context;
67
use assert_matches::assert_matches;
7-
use bdk_chain::{BlockId, CanonicalizationParams, ChainPosition, ConfirmationBlockTime};
8+
use bdk_chain::{
9+
keychain_txout::DEFAULT_LOOKAHEAD, BlockId, CanonicalizationParams, ChainPosition,
10+
ConfirmationBlockTime, DescriptorExt,
11+
};
812
use bdk_wallet::coin_selection::{self, LargestFirstCoinSelection};
913
use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor};
1014
use bdk_wallet::error::CreateTxError;
@@ -70,6 +74,7 @@ fn wallet_is_persisted() -> anyhow::Result<()> {
7074
let mut db = create_db(&file_path)?;
7175
let mut wallet = Wallet::create(external_desc, internal_desc)
7276
.network(Network::Testnet)
77+
.use_spk_cache(true)
7378
.create_wallet(&mut db)?;
7479
wallet.reveal_next_address(KeychainKind::External);
7580

@@ -79,13 +84,6 @@ fn wallet_is_persisted() -> anyhow::Result<()> {
7984
};
8085

8186
// recover wallet
82-
{
83-
let mut db = open_db(&file_path).context("failed to recover db")?;
84-
let _ = Wallet::load()
85-
.check_network(Network::Testnet)
86-
.load_wallet(&mut db)?
87-
.expect("wallet must exist");
88-
}
8987
{
9088
let mut db = open_db(&file_path).context("failed to recover db")?;
9189
let wallet = Wallet::load()
@@ -113,6 +111,67 @@ fn wallet_is_persisted() -> anyhow::Result<()> {
113111
.0
114112
);
115113
}
114+
// Test SPK cache
115+
{
116+
let mut db = open_db(&file_path).context("failed to recover db")?;
117+
let mut wallet = Wallet::load()
118+
.check_network(Network::Testnet)
119+
.use_spk_cache(true)
120+
.load_wallet(&mut db)?
121+
.expect("wallet must exist");
122+
123+
let external_did = wallet
124+
.public_descriptor(KeychainKind::External)
125+
.descriptor_id();
126+
let internal_did = wallet
127+
.public_descriptor(KeychainKind::Internal)
128+
.descriptor_id();
129+
130+
assert!(wallet.staged().is_none());
131+
132+
let _addr = wallet.reveal_next_address(KeychainKind::External);
133+
let cs = wallet.staged().expect("we should have staged a changeset");
134+
assert!(!cs.indexer.spk_cache.is_empty(), "failed to cache spks");
135+
assert_eq!(cs.indexer.spk_cache.len(), 2, "we persisted two keychains");
136+
let spk_cache: &BTreeMap<u32, ScriptBuf> =
137+
cs.indexer.spk_cache.get(&external_did).unwrap();
138+
assert_eq!(spk_cache.len() as u32, 1 + 1 + DEFAULT_LOOKAHEAD);
139+
assert_eq!(spk_cache.keys().last(), Some(&26));
140+
let spk_cache = cs.indexer.spk_cache.get(&internal_did).unwrap();
141+
assert_eq!(spk_cache.len() as u32, DEFAULT_LOOKAHEAD);
142+
assert_eq!(spk_cache.keys().last(), Some(&24));
143+
// Clear the stage
144+
let _ = wallet.take_staged();
145+
let _addr = wallet.reveal_next_address(KeychainKind::Internal);
146+
let cs = wallet.staged().unwrap();
147+
assert_eq!(cs.indexer.spk_cache.len(), 1);
148+
let spk_cache = cs.indexer.spk_cache.get(&internal_did).unwrap();
149+
assert_eq!(spk_cache.len(), 1);
150+
assert_eq!(spk_cache.keys().next(), Some(&25));
151+
}
152+
// SPK cache requires load params
153+
{
154+
let mut db = open_db(&file_path).context("failed to recover db")?;
155+
let mut wallet = Wallet::load()
156+
.check_network(Network::Testnet)
157+
// .use_spk_cache(false)
158+
.load_wallet(&mut db)?
159+
.expect("wallet must exist");
160+
161+
let internal_did = wallet
162+
.public_descriptor(KeychainKind::Internal)
163+
.descriptor_id();
164+
165+
assert!(wallet.staged().is_none());
166+
167+
let _addr = wallet.reveal_next_address(KeychainKind::Internal);
168+
let cs = wallet.staged().expect("we should have staged a changeset");
169+
assert_eq!(cs.indexer.last_revealed.get(&internal_did), Some(&0));
170+
assert!(
171+
cs.indexer.spk_cache.is_empty(),
172+
"we didn't set `use_spk_cache`"
173+
);
174+
}
116175

117176
Ok(())
118177
}
@@ -4406,7 +4465,7 @@ fn test_taproot_load_descriptor_duplicated_keys() {
44064465
#[test]
44074466
#[cfg(debug_assertions)]
44084467
#[should_panic(
4409-
expected = "replenish lookahead: must not have existing spk: keychain=Internal, lookahead=25, next_store_index=0, next_reveal_index=0"
4468+
expected = "replenish lookahead: must not have existing spk: keychain=Internal, lookahead=25, next_index=0"
44104469
)]
44114470
fn test_keychains_with_overlapping_spks() {
44124471
// this can happen if a non-wildcard descriptor keychain derives an spk that a

0 commit comments

Comments
 (0)