Skip to content

Commit fb7ff29

Browse files
committed
Merge #1203: Include the descriptor in keychain::Changeset
86711d4 doc(chain): add section for non-recommended K to descriptor assignments (Daniela Brozzoni) de53d72 test: Only the highest ord keychain is returned (Daniela Brozzoni) 9d8023b fix(chain): introduce keychain-variant-ranking to `KeychainTxOutIndex` (志宇) 6c87481 chore(chain): move `use` in `indexed_tx_graph.rs` so clippy is happy (志宇) 537aa03 chore(chain): update test so clippy does not complain (志宇) ed117de test(chain): applying changesets one-by-one vs aggregate should be same (志宇) 6a3fb84 fix(chain): simplify `Append::append` impl for `keychain::ChangeSet` (志宇) 1d294b7 fix: Run tests only if the miniscript feature is.. ..enabled, enable it by default (Daniela Brozzoni) 0e3e136 doc(bdk): Add instructions for manually inserting... ...secret keys in the wallet in Wallet::load (Daniela Brozzoni) 76afccc fix(wallet): add expected descriptors as signers after creating from wallet::ChangeSet (Steve Myers) 4f05441 keychain::ChangeSet includes the descriptor (Daniela Brozzoni) 8ff99f2 ref(chain): Define test descriptors, use them... ...everywhere (Daniela Brozzoni) b990293 ref(chain): move `keychain::ChangeSet` into `txout_index.rs` (志宇) Pull request description: Fixes #1101 - Moves keychain::ChangeSet inside `keychain/txout_index.rs` as now the `ChangeSet` depends on miniscript - Slightly cleans up tests by introducing some constant descriptors - The KeychainTxOutIndex's internal SpkIterator now uses DescriptorId instead of K. The DescriptorId -> K translation is made at the KeychainTxOutIndex level. - The keychain::Changeset is now a struct, which includes a map for last revealed indexes, and one for newly added keychains and their descriptor. ### Changelog notice API changes in bdk: - Wallet::keychains returns a `impl Iterator` instead of `BTreeMap` - Wallet::load doesn't take descriptors anymore, since they're stored in the db - Wallet::new_or_load checks if the loaded descriptor from db is the same as the provided one API changes in bdk_chain: - `ChangeSet` is now a struct, which includes a map for last revealed indexes, and one for keychains and descriptors. - `KeychainTxOutIndex::inner` returns a `SpkIterator<(DescriptorId, u32)>` - `KeychainTxOutIndex::outpoints` returns a `BTreeSet` instead of `&BTreeSet` - `KeychainTxOutIndex::keychains` returns a `impl Iterator` instead of `&BTreeMap` - `KeychainTxOutIndex::txouts` doesn't return a ExactSizeIterator anymore - `KeychainTxOutIndex::last_revealed_indices` returns a `BTreeMap` instead of `&BTreeMap` - `KeychainTxOutIndex::add_keychain` has been renamed to `KeychainTxOutIndex::insert_descriptor`, and now it returns a ChangeSet - `KeychainTxOutIndex::reveal_next_spk` returns Option - `KeychainTxOutIndex::next_unused_spk` returns Option - `KeychainTxOutIndex::unbounded_spk_iter` returns Option - `KeychainTxOutIndex::next_index` returns Option - `KeychainTxOutIndex::reveal_to_target` returns Option - `KeychainTxOutIndex::revealed_keychain_spks` returns Option - `KeychainTxOutIndex::unused_keychain_spks` returns Option - `KeychainTxOutIndex::last_revealed_index` returns Option - `KeychainTxOutIndex::keychain_outpoints` returns Option - `KeychainTxOutIndex::keychain_outpoints_in_range` returns Option - `KeychainTxOutIndex::last_used_index` returns None if the keychain has never been used, or if it doesn't exist ### 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 fmt` and `cargo clippy` before committing #### New Features: * [x] I've added tests for the new feature * [x] I've added docs for the new feature ACKs for top commit: evanlinjin: ACK 86711d4 Tree-SHA512: 4b1c9a31951f67b18037b7dd9837acbc35823f21de644ab833754b74d20f5373549f81e66965ecd3953ebf4f99644c9fd834812acfa65f9188950f1bda17ab60
2 parents 86408b9 + 86711d4 commit fb7ff29

File tree

20 files changed

+1236
-497
lines changed

20 files changed

+1236
-497
lines changed

crates/bdk/src/wallet/mod.rs

Lines changed: 214 additions & 68 deletions
Large diffs are not rendered by default.

crates/bdk/tests/wallet.rs

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::str::FromStr;
22

33
use assert_matches::assert_matches;
4-
use bdk::descriptor::calc_checksum;
4+
use bdk::descriptor::{calc_checksum, IntoWalletDescriptor};
55
use bdk::psbt::PsbtUtils;
66
use bdk::signer::{SignOptions, SignerError};
77
use bdk::wallet::coin_selection::{self, LargestFirstCoinSelection};
@@ -10,9 +10,11 @@ use bdk::wallet::tx_builder::AddForeignUtxoError;
1010
use bdk::wallet::NewError;
1111
use bdk::wallet::{AddressInfo, Balance, Wallet};
1212
use bdk::KeychainKind;
13+
use bdk_chain::collections::BTreeMap;
1314
use bdk_chain::COINBASE_MATURITY;
1415
use bdk_chain::{BlockId, ConfirmationTime};
1516
use bitcoin::hashes::Hash;
17+
use bitcoin::key::Secp256k1;
1618
use bitcoin::psbt;
1719
use bitcoin::script::PushBytesBuf;
1820
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
@@ -84,14 +86,24 @@ fn load_recovers_wallet() {
8486
// recover wallet
8587
{
8688
let db = bdk_file_store::Store::open(DB_MAGIC, &file_path).expect("must recover db");
87-
let wallet =
88-
Wallet::load(get_test_tr_single_sig_xprv(), None, db).expect("must recover wallet");
89+
let wallet = Wallet::load(db).expect("must recover wallet");
8990
assert_eq!(wallet.network(), Network::Testnet);
90-
assert_eq!(wallet.spk_index().keychains(), wallet_spk_index.keychains());
91+
assert_eq!(
92+
wallet.spk_index().keychains().collect::<Vec<_>>(),
93+
wallet_spk_index.keychains().collect::<Vec<_>>()
94+
);
9195
assert_eq!(
9296
wallet.spk_index().last_revealed_indices(),
9397
wallet_spk_index.last_revealed_indices()
9498
);
99+
let secp = Secp256k1::new();
100+
assert_eq!(
101+
*wallet.get_descriptor_for_keychain(KeychainKind::External),
102+
get_test_tr_single_sig_xprv()
103+
.into_wallet_descriptor(&secp, wallet.network())
104+
.unwrap()
105+
.0
106+
);
95107
}
96108

97109
// `new` can only be called on empty db
@@ -108,12 +120,12 @@ fn new_or_load() {
108120
let file_path = temp_dir.path().join("store.db");
109121

110122
// init wallet when non-existent
111-
let wallet_keychains = {
123+
let wallet_keychains: BTreeMap<_, _> = {
112124
let db = bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path)
113125
.expect("must create db");
114126
let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet)
115127
.expect("must init wallet");
116-
wallet.keychains().clone()
128+
wallet.keychains().map(|(k, v)| (*k, v.clone())).collect()
117129
};
118130

119131
// wrong network
@@ -162,14 +174,60 @@ fn new_or_load() {
162174
);
163175
}
164176

177+
// wrong external descriptor
178+
{
179+
let exp_descriptor = get_test_tr_single_sig();
180+
let got_descriptor = get_test_wpkh()
181+
.into_wallet_descriptor(&Secp256k1::new(), Network::Testnet)
182+
.unwrap()
183+
.0;
184+
185+
let db =
186+
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
187+
let err = Wallet::new_or_load(exp_descriptor, None, db, Network::Testnet)
188+
.expect_err("wrong external descriptor");
189+
assert!(
190+
matches!(
191+
err,
192+
bdk::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
193+
if got == &Some(got_descriptor) && keychain == KeychainKind::External
194+
),
195+
"err: {}",
196+
err,
197+
);
198+
}
199+
200+
// wrong internal descriptor
201+
{
202+
let exp_descriptor = Some(get_test_tr_single_sig());
203+
let got_descriptor = None;
204+
205+
let db =
206+
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
207+
let err = Wallet::new_or_load(get_test_wpkh(), exp_descriptor, db, Network::Testnet)
208+
.expect_err("wrong internal descriptor");
209+
assert!(
210+
matches!(
211+
err,
212+
bdk::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
213+
if got == &got_descriptor && keychain == KeychainKind::Internal
214+
),
215+
"err: {}",
216+
err,
217+
);
218+
}
219+
165220
// all parameters match
166221
{
167222
let db =
168223
bdk_file_store::Store::open_or_create_new(DB_MAGIC, &file_path).expect("must open db");
169224
let wallet = Wallet::new_or_load(get_test_wpkh(), None, db, Network::Testnet)
170225
.expect("must recover wallet");
171226
assert_eq!(wallet.network(), Network::Testnet);
172-
assert_eq!(wallet.keychains(), &wallet_keychains);
227+
assert!(wallet
228+
.keychains()
229+
.map(|(k, v)| (*k, v.clone()))
230+
.eq(wallet_keychains));
173231
}
174232
}
175233

@@ -181,7 +239,6 @@ fn test_descriptor_checksum() {
181239

182240
let raw_descriptor = wallet
183241
.keychains()
184-
.iter()
185242
.next()
186243
.unwrap()
187244
.1

crates/chain/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,6 @@ rand = "0.8"
2626
proptest = "1.2.0"
2727

2828
[features]
29-
default = ["std"]
30-
std = ["bitcoin/std", "miniscript/std"]
31-
serde = ["serde_crate", "bitcoin/serde"]
29+
default = ["std", "miniscript"]
30+
std = ["bitcoin/std", "miniscript?/std"]
31+
serde = ["serde_crate", "bitcoin/serde", "miniscript?/serde"]

crates/chain/src/descriptor_ext.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
1-
use crate::miniscript::{Descriptor, DescriptorPublicKey};
1+
use crate::{
2+
alloc::{string::ToString, vec::Vec},
3+
miniscript::{Descriptor, DescriptorPublicKey},
4+
};
5+
use bitcoin::hashes::{hash_newtype, sha256, Hash};
6+
7+
hash_newtype! {
8+
/// Represents the ID of a descriptor, defined as the sha256 hash of
9+
/// the descriptor string, checksum excluded.
10+
///
11+
/// This is useful for having a fixed-length unique representation of a descriptor,
12+
/// in particular, we use it to persist application state changes related to the
13+
/// descriptor without having to re-write the whole descriptor each time.
14+
///
15+
pub struct DescriptorId(pub sha256::Hash);
16+
}
217

318
/// A trait to extend the functionality of a miniscript descriptor.
419
pub trait DescriptorExt {
520
/// Returns the minimum value (in satoshis) at which an output is broadcastable.
621
/// Panics if the descriptor wildcard is hardened.
722
fn dust_value(&self) -> u64;
23+
24+
/// Returns the descriptor id, calculated as the sha256 of the descriptor, checksum not
25+
/// included.
26+
fn descriptor_id(&self) -> DescriptorId;
827
}
928

1029
impl DescriptorExt for Descriptor<DescriptorPublicKey> {
@@ -15,4 +34,11 @@ impl DescriptorExt for Descriptor<DescriptorPublicKey> {
1534
.dust_value()
1635
.to_sat()
1736
}
37+
38+
fn descriptor_id(&self) -> DescriptorId {
39+
let desc = self.to_string();
40+
let desc_without_checksum = desc.split('#').next().expect("Must be here");
41+
let descriptor_bytes = <Vec<u8>>::from(desc_without_checksum.as_bytes());
42+
DescriptorId(sha256::Hash::hash(&descriptor_bytes))
43+
}
1844
}

crates/chain/src/indexed_tx_graph.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use alloc::vec::Vec;
44
use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
55

66
use crate::{
7-
keychain,
87
tx_graph::{self, TxGraph},
98
Anchor, AnchorFromBlockPosition, Append, BlockId,
109
};
@@ -320,8 +319,9 @@ impl<A, IA: Default> From<tx_graph::ChangeSet<A>> for ChangeSet<A, IA> {
320319
}
321320
}
322321

323-
impl<A, K> From<keychain::ChangeSet<K>> for ChangeSet<A, keychain::ChangeSet<K>> {
324-
fn from(indexer: keychain::ChangeSet<K>) -> Self {
322+
#[cfg(feature = "miniscript")]
323+
impl<A, K> From<crate::keychain::ChangeSet<K>> for ChangeSet<A, crate::keychain::ChangeSet<K>> {
324+
fn from(indexer: crate::keychain::ChangeSet<K>) -> Self {
325325
Self {
326326
graph: Default::default(),
327327
indexer,

crates/chain/src/keychain.rs

Lines changed: 0 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -10,78 +10,12 @@
1010
//!
1111
//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex
1212
13-
use crate::{collections::BTreeMap, Append};
14-
1513
#[cfg(feature = "miniscript")]
1614
mod txout_index;
1715
use bitcoin::Amount;
1816
#[cfg(feature = "miniscript")]
1917
pub use txout_index::*;
2018

21-
/// Represents updates to the derivation index of a [`KeychainTxOutIndex`].
22-
/// It maps each keychain `K` to its last revealed index.
23-
///
24-
/// It can be applied to [`KeychainTxOutIndex`] with [`apply_changeset`]. [`ChangeSet`]s are
25-
/// monotone in that they will never decrease the revealed derivation index.
26-
///
27-
/// [`KeychainTxOutIndex`]: crate::keychain::KeychainTxOutIndex
28-
/// [`apply_changeset`]: crate::keychain::KeychainTxOutIndex::apply_changeset
29-
#[derive(Clone, Debug, PartialEq)]
30-
#[cfg_attr(
31-
feature = "serde",
32-
derive(serde::Deserialize, serde::Serialize),
33-
serde(
34-
crate = "serde_crate",
35-
bound(
36-
deserialize = "K: Ord + serde::Deserialize<'de>",
37-
serialize = "K: Ord + serde::Serialize"
38-
)
39-
)
40-
)]
41-
#[must_use]
42-
pub struct ChangeSet<K>(pub BTreeMap<K, u32>);
43-
44-
impl<K> ChangeSet<K> {
45-
/// Get the inner map of the keychain to its new derivation index.
46-
pub fn as_inner(&self) -> &BTreeMap<K, u32> {
47-
&self.0
48-
}
49-
}
50-
51-
impl<K: Ord> Append for ChangeSet<K> {
52-
/// Append another [`ChangeSet`] into self.
53-
///
54-
/// If the keychain already exists, increase the index when the other's index > self's index.
55-
/// If the keychain did not exist, append the new keychain.
56-
fn append(&mut self, mut other: Self) {
57-
self.0.iter_mut().for_each(|(key, index)| {
58-
if let Some(other_index) = other.0.remove(key) {
59-
*index = other_index.max(*index);
60-
}
61-
});
62-
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
63-
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
64-
self.0.extend(other.0);
65-
}
66-
67-
/// Returns whether the changeset are empty.
68-
fn is_empty(&self) -> bool {
69-
self.0.is_empty()
70-
}
71-
}
72-
73-
impl<K> Default for ChangeSet<K> {
74-
fn default() -> Self {
75-
Self(Default::default())
76-
}
77-
}
78-
79-
impl<K> AsRef<BTreeMap<K, u32>> for ChangeSet<K> {
80-
fn as_ref(&self) -> &BTreeMap<K, u32> {
81-
&self.0
82-
}
83-
}
84-
8519
/// Balance, differentiated into various categories.
8620
#[derive(Debug, PartialEq, Eq, Clone, Default)]
8721
#[cfg_attr(
@@ -137,40 +71,3 @@ impl core::ops::Add for Balance {
13771
}
13872
}
13973
}
140-
141-
#[cfg(test)]
142-
mod test {
143-
use super::*;
144-
145-
#[test]
146-
fn append_keychain_derivation_indices() {
147-
#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
148-
enum Keychain {
149-
One,
150-
Two,
151-
Three,
152-
Four,
153-
}
154-
let mut lhs_di = BTreeMap::<Keychain, u32>::default();
155-
let mut rhs_di = BTreeMap::<Keychain, u32>::default();
156-
lhs_di.insert(Keychain::One, 7);
157-
lhs_di.insert(Keychain::Two, 0);
158-
rhs_di.insert(Keychain::One, 3);
159-
rhs_di.insert(Keychain::Two, 5);
160-
lhs_di.insert(Keychain::Three, 3);
161-
rhs_di.insert(Keychain::Four, 4);
162-
163-
let mut lhs = ChangeSet(lhs_di);
164-
let rhs = ChangeSet(rhs_di);
165-
lhs.append(rhs);
166-
167-
// Exiting index doesn't update if the new index in `other` is lower than `self`.
168-
assert_eq!(lhs.0.get(&Keychain::One), Some(&7));
169-
// Existing index updates if the new index in `other` is higher than `self`.
170-
assert_eq!(lhs.0.get(&Keychain::Two), Some(&5));
171-
// Existing index is unchanged if keychain doesn't exist in `other`.
172-
assert_eq!(lhs.0.get(&Keychain::Three), Some(&3));
173-
// New keychain gets added if the keychain is in `other` but not in `self`.
174-
assert_eq!(lhs.0.get(&Keychain::Four), Some(&4));
175-
}
176-
}

0 commit comments

Comments
 (0)