Skip to content

Commit af0e9eb

Browse files
committed
feat!: check desc, return changeset in add_desc fn
Incorporated `check_wallet_descriptor` in `KeyRing::new` and `KeyRing::add_descriptor` to catch multipath and hardened descriptors. Introduced new variants to `DescriptorError` to report cases when trying to add duplicate desc or keychain. Did not provide the other desc inside `KeychainAlreadyExists` variant (similar other keychain inside `DescAlreadyExists`) because introducing type parameter to `DescriptorError` implies introducing one to `IntoWalletDescriptor`. The add_descriptor function returns a `KeyRing::Changeset` since this would eventually be required when we introducing APIs to add descriptors to `Wallet.keyring`.
1 parent 603b1cf commit af0e9eb

File tree

6 files changed

+135
-50
lines changed

6 files changed

+135
-50
lines changed

examples/multi_keychain_wallet.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,12 @@ fn main() {
5858
time_of_week_keychain: DayType::AnyDay,
5959
};
6060

61-
let mut keyring: KeyRing<KeychainId> = KeyRing::new(Network::Signet, keychain_1, DESC_1);
62-
keyring.add_descriptor(keychain_2, DESC_2, false);
63-
keyring.add_descriptor(keychain_3, DESC_3, false);
64-
keyring.add_descriptor(keychain_4, DESC_4, false);
65-
keyring.add_descriptor(keychain_5, DESC_5, false);
61+
let mut keyring: KeyRing<KeychainId> =
62+
KeyRing::new(Network::Signet, keychain_1, DESC_1).unwrap();
63+
keyring.add_descriptor(keychain_2, DESC_2, false).unwrap();
64+
keyring.add_descriptor(keychain_3, DESC_3, false).unwrap();
65+
keyring.add_descriptor(keychain_4, DESC_4, false).unwrap();
66+
keyring.add_descriptor(keychain_5, DESC_5, false).unwrap();
6667

6768
// DESC_1 is the default keychain (the first one added to the keyring is automatically the
6869
// default keychain), but this can also be changed later on with the

examples/simple_keyring.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ fn main() {
1313
Network::Regtest,
1414
KeychainKind::External,
1515
EXTERNAL_DESCRIPTOR,
16-
);
17-
keyring.add_descriptor(KeychainKind::Internal, INTERNAL_DESCRIPTOR, false);
16+
)
17+
.unwrap();
18+
keyring
19+
.add_descriptor(KeychainKind::Internal, INTERNAL_DESCRIPTOR, false)
20+
.unwrap();
1821

1922
let mut wallet = Wallet::new(keyring);
2023
let address = wallet.reveal_next_address(KeychainKind::External).unwrap();

src/descriptor/error.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ pub enum Error {
2828
Key(crate::keys::KeyError),
2929
// /// Error while extracting and manipulating policies
3030
// Policy(crate::descriptor::policy::PolicyError),
31-
3231
/// Invalid byte found in the descriptor checksum
3332
InvalidDescriptorCharacter(u8),
3433

@@ -42,8 +41,10 @@ pub enum Error {
4241
Miniscript(miniscript::Error),
4342
/// Hex decoding error
4443
Hex(bitcoin::hex::HexToBytesError),
45-
/// The provided wallet descriptors are identical
46-
ExternalAndInternalAreTheSame,
44+
/// The keychain exists in the `KeyRing` but mapped to a different descriptor
45+
KeychainAlreadyExists,
46+
/// The descriptor exists in the `KeyRing` but mapped to a different keychain
47+
DescAlreadyExists,
4748
}
4849

4950
impl From<crate::keys::KeyError> for Error {
@@ -81,9 +82,8 @@ impl fmt::Display for Error {
8182
Self::Pk(err) => write!(f, "Key-related error: {err}"),
8283
Self::Miniscript(err) => write!(f, "Miniscript error: {err}"),
8384
Self::Hex(err) => write!(f, "Hex decoding error: {err}"),
84-
Self::ExternalAndInternalAreTheSame => {
85-
write!(f, "External and internal descriptors are the same")
86-
}
85+
Self::KeychainAlreadyExists => write!(f, "Keychain already exists but corresponds to a different descriptor"),
86+
Self::DescAlreadyExists => write!(f, "Descriptor already exists but corresponds to a different keychain"),
8787
}
8888
}
8989
}

src/keyring/mod.rs

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
/// Contains `Changeset` corresponding to `KeyRing`.
1717
pub mod changeset;
1818

19-
use crate::descriptor::IntoWalletDescriptor;
19+
use crate::descriptor::check_wallet_descriptor;
20+
use crate::descriptor::{DescriptorError, IntoWalletDescriptor};
2021
use crate::keyring::changeset::ChangeSet;
2122
use alloc::collections::BTreeMap;
2223
use bitcoin::secp256k1::{All, Secp256k1};
@@ -38,46 +39,65 @@ where
3839
{
3940
/// Construct a new [`KeyRing`] with the provided `network` and a descriptor. This descriptor
4041
/// will automatically become your default keychain. You can change your default keychain
41-
/// upon adding new ones with [`KeyRing::add_descriptor`]. Note that you cannot use a
42-
/// multipath descriptor here.
43-
pub fn new(network: Network, keychain: K, descriptor: impl IntoWalletDescriptor) -> Self {
42+
/// upon adding new ones with [`KeyRing::add_descriptor`].
43+
///
44+
/// This method returns [`DescriptorError`] if the provided descriptor is multipath (in case of
45+
/// public descriptors), contains hardened derivation steps or fails miniscripts sanity
46+
/// checks.
47+
pub fn new(
48+
network: Network,
49+
keychain: K,
50+
descriptor: impl IntoWalletDescriptor,
51+
) -> Result<Self, DescriptorError> {
4452
let secp = Secp256k1::new();
45-
let descriptor = descriptor
46-
.into_wallet_descriptor(&secp, network)
47-
.expect("err: invalid descriptor")
48-
.0;
49-
assert!(
50-
!descriptor.is_multipath(),
51-
"err: Use `add_multipath_descriptor` instead"
52-
);
53-
Self {
53+
let descriptor = descriptor.into_wallet_descriptor(&secp, network)?.0;
54+
check_wallet_descriptor(&descriptor)?;
55+
Ok(Self {
5456
secp: Secp256k1::new(),
5557
network,
5658
descriptors: BTreeMap::from([(keychain.clone(), descriptor)]),
5759
default_keychain: keychain.clone(),
58-
}
60+
})
5961
}
6062

6163
/// Add a descriptor. Must not be [multipath](miniscript::Descriptor::is_multipath).
64+
/// This method returns [`DescriptorError`] if the provided descriptor is multipath, contains
65+
/// hardened derivation steps (in case of public descriptors) or fails miniscripts sanity
66+
/// checks. It also returns the error when exactly one of `keychain` or `descriptor` is
67+
/// already in the keyring.
6268
pub fn add_descriptor(
6369
&mut self,
6470
keychain: K,
6571
descriptor: impl IntoWalletDescriptor,
6672
default: bool,
67-
) {
73+
) -> Result<ChangeSet<K>, DescriptorError> {
6874
let descriptor = descriptor
69-
.into_wallet_descriptor(&self.secp, self.network)
70-
.expect("err: invalid descriptor")
75+
.into_wallet_descriptor(&self.secp, self.network)?
7176
.0;
72-
assert!(
73-
!descriptor.is_multipath(),
74-
"err: Use `add_multipath_descriptor` instead"
75-
);
77+
check_wallet_descriptor(&descriptor)?;
78+
79+
// if the descriptor or keychain already exist
80+
for (keychain_old, desc) in self.descriptors.iter() {
81+
if (desc == &descriptor) && (keychain_old != &keychain) {
82+
return Err(DescriptorError::DescAlreadyExists);
83+
}
84+
if (keychain_old == &keychain) && (desc != &descriptor) {
85+
return Err(DescriptorError::KeychainAlreadyExists);
86+
}
87+
}
88+
89+
self.descriptors
90+
.insert(keychain.clone(), descriptor.clone());
91+
92+
let mut changeset = ChangeSet::default();
93+
changeset.descriptors.insert(keychain.clone(), descriptor);
7694

7795
if default {
7896
self.default_keychain = keychain.clone();
97+
changeset.default_keychain = Some(keychain);
7998
}
80-
self.descriptors.insert(keychain, descriptor);
99+
100+
Ok(changeset)
81101
}
82102

83103
/// Returns the specified default keychain on the KeyRing.

src/wallet/mod.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,15 @@ pub mod tx_builder;
5959
pub(crate) mod utils;
6060

6161
use crate::descriptor::{
62-
check_wallet_descriptor, error::Error as DescriptorError,
62+
check_wallet_descriptor,
63+
error::Error as DescriptorError,
6364
// policy::BuildSatisfaction,
64-
DerivedDescriptor, DescriptorMeta, ExtendedDescriptor,
65-
// ExtractPolicy,
65+
DerivedDescriptor,
66+
DescriptorMeta,
67+
ExtendedDescriptor,
68+
// ExtractPolicy,
6669
IntoWalletDescriptor,
67-
// Policy,
70+
// Policy,
6871
XKeyUtils,
6972
};
7073
use crate::keyring::KeyRing;
@@ -3002,10 +3005,10 @@ mod test {
30023005
fn test_keyring(desc_strs: impl IntoIterator<Item = &'static str>) -> KeyRing<DescriptorId> {
30033006
let mut desc_strs = desc_strs.into_iter();
30043007
let desc = parse_descriptor(desc_strs.next().unwrap());
3005-
let mut keyring = KeyRing::new(Network::Testnet4, desc.descriptor_id(), desc);
3008+
let mut keyring = KeyRing::new(Network::Testnet4, desc.descriptor_id(), desc).unwrap();
30063009
for desc_str in desc_strs {
30073010
let desc = parse_descriptor(desc_str);
3008-
keyring.add_descriptor(desc.descriptor_id(), desc, false);
3011+
let _ = keyring.add_descriptor(desc.descriptor_id(), desc, false);
30093012
}
30103013
keyring
30113014
}

tests/keyring.rs

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1+
use bdk_wallet::chain::DescriptorExt;
2+
use bdk_wallet::descriptor::DescriptorError;
13
use bdk_wallet::keyring::KeyRing;
24
use bdk_wallet::KeychainKind;
5+
use bitcoin::secp256k1::Secp256k1;
36
use bitcoin::Network;
7+
use miniscript::{Descriptor, DescriptorPublicKey};
48

59
// From the mnemonic "awesome awesome awesome awesome awesome awesome awesome awesome awesome
610
// awesome awesome awesome"
711
const DESC_1: &str = "tr(tprv8ZgxMBicQKsPdWAHbugK2tjtVtRjKGixYVZUdL7xLHMgXZS6BFbFi1UDb1CHT25Z5PU1F9j7wGxwUiRhqz9E3nZRztikGUV6HoRDYcqPhM4/86'/1'/0'/0/*)";
812

13+
pub fn get_descriptor(desc_str: &str) -> Descriptor<DescriptorPublicKey> {
14+
Descriptor::parse_descriptor(&Secp256k1::new(), desc_str)
15+
.unwrap()
16+
.0
17+
}
18+
919
#[test]
1020
fn test_simple_keyring() {
1121
let network = Network::Regtest;
1222
let keychain_id = KeychainKind::External;
1323

14-
let keyring = KeyRing::new(network, keychain_id, DESC_1);
24+
let keyring = KeyRing::new(network, keychain_id, DESC_1).unwrap();
1525

1626
assert_eq!(keyring.default_keychain(), keychain_id);
1727
assert_eq!(keyring.list_keychains().len(), 1);
@@ -28,15 +38,63 @@ fn test_8_keychains_keyring() {
2838
const DESC_7: &str = "tr(tprv8ZgxMBicQKsPdWAHbugK2tjtVtRjKGixYVZUdL7xLHMgXZS6BFbFi1UDb1CHT25Z5PU1F9j7wGxwUiRhqz9E3nZRztikGUV6HoRDYcqPhM4/86'/1'/0'/6/*)";
2939
const DESC_8: &str = "tr(tprv8ZgxMBicQKsPdWAHbugK2tjtVtRjKGixYVZUdL7xLHMgXZS6BFbFi1UDb1CHT25Z5PU1F9j7wGxwUiRhqz9E3nZRztikGUV6HoRDYcqPhM4/86'/1'/0'/7/*)";
3040

31-
let mut keyring = KeyRing::new(Network::Regtest, 1, DESC_1);
32-
keyring.add_descriptor(2, DESC_2, false);
33-
keyring.add_descriptor(3, DESC_3, false);
34-
keyring.add_descriptor(4, DESC_4, false);
35-
keyring.add_descriptor(5, DESC_5, false);
36-
keyring.add_descriptor(6, DESC_6, false);
37-
keyring.add_descriptor(7, DESC_7, false);
38-
keyring.add_descriptor(8, DESC_8, false);
41+
let mut keyring = KeyRing::new(Network::Regtest, 1, DESC_1).unwrap();
42+
keyring.add_descriptor(2, DESC_2, false).unwrap();
43+
keyring.add_descriptor(3, DESC_3, false).unwrap();
44+
keyring.add_descriptor(4, DESC_4, false).unwrap();
45+
keyring.add_descriptor(5, DESC_5, false).unwrap();
46+
keyring.add_descriptor(6, DESC_6, false).unwrap();
47+
keyring.add_descriptor(7, DESC_7, false).unwrap();
48+
keyring.add_descriptor(8, DESC_8, false).unwrap();
3949

4050
assert_eq!(keyring.default_keychain(), 1);
4151
assert_eq!(keyring.list_keychains().len(), 8);
4252
}
53+
54+
#[test]
55+
fn err_on_hardened_derivation_path() {
56+
let err = KeyRing::new(Network::Regtest, KeychainKind::External, "tr(tpubD6NzVbkrYhZ4WyC5VZLuSJQ14uwfUbus7oAFurAFkZA5N3groeQqtW65m8pG1TT1arPpfWu9RbBsc5rSBncrX2d84BAwJJHQfaRjnMCQwuT/86h/1h/0h/0/*)").err();
57+
assert_eq!(err, Some(DescriptorError::HardenedDerivationXpub));
58+
let mut keyring = KeyRing::new(Network::Regtest, KeychainKind::External, DESC_1).unwrap();
59+
let res = keyring.add_descriptor(KeychainKind::Internal,"tr([738b4dbd/86h/1h/0h]tpubDDQsJyQKuP6jCCSZ75Y8zpBAnXsvAN6BWpp6ZoczfxKBDBWnY8XGbC7AMMSyXAcQPNgppkCBmv3hkCLZSaQ4VvSTGsstuTrXuDadMaB7E45/0'/*)", false).err();
60+
assert_eq!(res, Some(DescriptorError::HardenedDerivationXpub));
61+
}
62+
63+
#[test]
64+
fn err_on_multipath() {
65+
let err = KeyRing::new(Network::Regtest, KeychainKind::External, "pkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/<0;1>)").err();
66+
assert_eq!(
67+
err,
68+
Some(DescriptorError::Miniscript(
69+
miniscript::Error::BadDescriptor(
70+
"`check_wallet_descriptor` must not contain multipath keys".to_string(),
71+
)
72+
))
73+
);
74+
let mut keyring = KeyRing::new(Network::Regtest, KeychainKind::External, DESC_1).unwrap();
75+
let res = keyring.add_descriptor(KeychainKind::Internal, "tr(tpubD6NzVbkrYhZ4WyC5VZLuSJQ14uwfUbus7oAFurAFkZA5N3groeQqtW65m8pG1TT1arPpfWu9RbBsc5rSBncrX2d84BAwJJHQfaRjnMCQwuT/86/1/0/<0;1>/*)" , false).err();
76+
assert_eq!(
77+
res,
78+
Some(DescriptorError::Miniscript(
79+
miniscript::Error::BadDescriptor(
80+
"`check_wallet_descriptor` must not contain multipath keys".to_string(),
81+
)
82+
))
83+
);
84+
}
85+
86+
#[test]
87+
fn duplicate_desc_keychain() {
88+
let desc1 = get_descriptor(DESC_1);
89+
let mut keyring = KeyRing::new(Network::Regtest, desc1.descriptor_id(), desc1.clone()).unwrap();
90+
let desc2 = get_descriptor("tr(tprv8ZgxMBicQKsPdWAHbugK2tjtVtRjKGixYVZUdL7xLHMgXZS6BFbFi1UDb1CHT25Z5PU1F9j7wGxwUiRhqz9E3nZRztikGUV6HoRDYcqPhM4/86'/1'/0'/1/*)");
91+
let res1 = keyring
92+
.add_descriptor(desc2.descriptor_id(), desc1.clone(), false)
93+
.err();
94+
assert_eq!(res1, Some(DescriptorError::DescAlreadyExists));
95+
96+
let res2 = keyring
97+
.add_descriptor(desc1.descriptor_id(), desc2, false)
98+
.err();
99+
assert_eq!(res2, Some(DescriptorError::KeychainAlreadyExists));
100+
}

0 commit comments

Comments
 (0)