From 22ee2f077b06f573e6eb6a9fec0c7695c2a405e9 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 17:59:36 +0200 Subject: [PATCH 01/18] descriptor: document an error case for at_derivation_index Source: rust-miniscript 3d9acd2 Co-authored-by: Nadav Ivgi --- src/descriptor/key.rs | 1 + src/descriptor/mod.rs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index 7e457ced..8df5f836 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -692,6 +692,7 @@ impl DescriptorPublicKey { /// # Errors /// /// - If `index` is hardened. + /// - If the key contains multi-path derivations pub fn at_derivation_index(self, index: u32) -> Result { let definite = match self { DescriptorPublicKey::Single(_) => self, diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 68f67928..0e254f1e 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -824,6 +824,7 @@ impl Descriptor { /// /// # Errors /// - If index ≥ 2^31 + /// - If the descriptor contains multi-path derivations pub fn at_derivation_index( &self, index: u32, @@ -876,7 +877,8 @@ impl Descriptor { /// /// # Errors /// - /// This function will return an error if hardened derivation is attempted. + /// This function will return an error for multi-path descriptors + /// or if hardened derivation is attempted, pub fn derived_descriptor( &self, secp: &secp256k1_zkp::Secp256k1, From 4ab5fc63936741124ac7ecb6f0d2177b9c2787c6 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 17:47:29 +0200 Subject: [PATCH 02/18] descriptor: support conversion of multi-xprivs into multi-xpubs Port of upstream "Support conversion of multi-Xprivs into multi-Xpubs" Upstream PR #757 also has ae64ce6 ("Some minor formatting changes"), which only reformats the origin matches of the two to_public functions. It is not ported: our key.rs keeps its own formatting, and this commit takes the 7d3e1e8 code as it was before that reformat. Source: rust-miniscript 7d3e1e8 Co-authored-by: Nadav Ivgi --- src/descriptor/key.rs | 108 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 7 deletions(-) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index 8df5f836..8aec5db9 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -305,6 +305,89 @@ impl DescriptorXKey { } } +impl DescriptorMultiXKey { + /// Returns the public version of this multi-key, applying all the hardened derivation steps that + /// are shared among all derivation paths before turning it into a public key. + /// + /// Errors if there are hardened derivation steps that are not shared among all paths. + fn to_public( + &self, + secp: &Secp256k1, + ) -> Result, DescriptorKeyParseError> { + let deriv_paths = self.derivation_paths.paths(); + + let shared_prefix: Vec<_> = deriv_paths[0] + .into_iter() + .enumerate() + .take_while(|(index, child_num)| { + deriv_paths[1..].iter().all(|other_path| { + other_path.len() > *index && other_path[*index] == **child_num + }) + }) + .map(|(_, child_num)| *child_num) + .collect(); + + let suffixes: Vec> = deriv_paths + .iter() + .map(|path| { + path.into_iter() + .skip(shared_prefix.len()) + .map(|child_num| { + if child_num.is_normal() { + Ok(*child_num) + } else { + Err(DescriptorKeyParseError("Can't make a multi-xpriv with hardened derivation steps that are not shared among all paths into a public key.")) + } + }) + .collect() + }) + .collect::>()?; + + let unhardened = shared_prefix + .iter() + .rev() + .take_while(|c| c.is_normal()) + .count(); + let last_hardened_idx = shared_prefix.len() - unhardened; + let hardened_path = &shared_prefix[..last_hardened_idx]; + let unhardened_path = &shared_prefix[last_hardened_idx..]; + + let xprv = self + .xkey + .derive_priv(secp, &hardened_path) + .map_err(|_| DescriptorKeyParseError("Unable to derive the hardened steps"))?; + let xpub = bip32::Xpub::from_priv(secp, &xprv); + + let origin = match &self.origin { + Some((fingerprint, path)) => Some(( + *fingerprint, + path.into_iter() + .chain(hardened_path.iter()) + .copied() + .collect(), + )), + None if !hardened_path.is_empty() => { + Some((self.xkey.fingerprint(secp), hardened_path.into())) + } + None => None, + }; + let new_deriv_paths = suffixes + .into_iter() + .map(|suffix| { + let path = unhardened_path.iter().copied().chain(suffix); + path.collect::>().into() + }) + .collect(); + + Ok(DescriptorMultiXKey { + origin, + xkey: xpub, + derivation_paths: DerivPaths::new(new_deriv_paths).expect("not empty"), + wildcard: self.wildcard, + }) + } +} + impl fmt::Display for DescriptorXKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { maybe_fmt_master_id(f, &self.origin)?; @@ -394,9 +477,8 @@ impl DescriptorSecretKey { /// If the key is an "XPrv", the hardened derivation steps will be applied /// before converting it to a public key. /// - /// It will return an error if the key is a "multi-xpriv", as we wouldn't - /// always be able to apply hardened derivation steps if there are multiple - /// paths. + /// It will return an error if the key is a "multi-xpriv" that includes + /// hardened derivation steps not shared for all paths. pub fn to_public( &self, secp: &Secp256k1, @@ -404,10 +486,8 @@ impl DescriptorSecretKey { let pk = match self { DescriptorSecretKey::Single(prv) => DescriptorPublicKey::Single(prv.to_public(secp)), DescriptorSecretKey::XPrv(xprv) => DescriptorPublicKey::XPub(xprv.to_public(secp)?), - DescriptorSecretKey::MultiXPrv(_) => { - return Err(DescriptorKeyParseError( - "Can't make an extended private key with multiple paths into a public key.", - )) + DescriptorSecretKey::MultiXPrv(xprv) => { + DescriptorPublicKey::MultiXPub(xprv.to_public(secp)?) } }; @@ -1630,6 +1710,20 @@ mod test { DescriptorPublicKey::from_str("tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi/2/4/<0;1;>").unwrap_err(); } + #[test] + fn test_multixprv_to_public() { + let secp = secp256k1::Secp256k1::signing_only(); + + // Works if all hardended derivation steps are part of the shared path + let xprv = get_multipath_xprv("[01020304/5]tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/1'/2'/3/<4;5>/6"); + let xpub = DescriptorPublicKey::MultiXPub(xprv.to_public(&secp).unwrap()); // wrap in a DescriptorPublicKey to have Display + assert_eq!(xpub.to_string(), "[01020304/5/1'/2']tpubDBTRkEMEFkUbk3WTz6CFSULyswkTPpPr38AWibf5TVkB5GxuBxbSbmdFGr3jmswwemknyYxAGoX7BJnKfyPy4WXaHmcrxZhfzFwoUFvFtm5/3/<4;5>/6"); + + // Fails if they're part of the multi-path specifier or following it + get_multipath_xprv("tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/1/2/<3';4'>/5").to_public(&secp).unwrap_err(); + get_multipath_xprv("tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/1/2/<3;4>/5/6'").to_public(&secp).unwrap_err(); + } + #[test] #[cfg(feature = "serde")] fn test_descriptor_public_key_serde() { From d61f3c9761f50ef75ccb39a7663fb1817eb331a2 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 17:50:10 +0200 Subject: [PATCH 03/18] descriptor: make DescriptorKeyParseError an enum Port of upstream "refactor: make descriptors::key::DescriptorKeyParseError an enum" This is a prerequisite of the NonDefiniteKeyError port: upstream adds DescriptorKeyParseError::NonDefiniteKey and MalformedKeyDataKind::MaximumDepthExceeded to these enums. Elements adaptations: - The hex private key of a ct() view key, which upstream does not parse, is reported with a new Elements-only DescriptorKeyParseError::HexPrivateKey variant. The key is parsed with secp256k1::SecretKey::from_str, so that one error type covers both bad hex and an invalid secret key. - The confidential_descriptor_invalid test expects the new "key too short" message. Source: rust-miniscript 497c5a3 Co-authored-by: Chris Ricketts --- src/confidential/mod.rs | 2 +- src/descriptor/key.rs | 343 ++++++++++++++++++++++++++++------------ src/descriptor/mod.rs | 2 +- 3 files changed, 240 insertions(+), 107 deletions(-) diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 4e8703e0..7e613852 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -487,7 +487,7 @@ mod tests { ), ( "ct(L3jXxwef3fpB7hcrFozcWgHeJCPSAFiZ1Ji2YJMPxceaGvy3PC1q,elwpkh(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4))#gcy6hcfz", - "unexpected «Key too short (<66 char), doesn't match any format»", + "unexpected «key too short»", ), ]; diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index 8aec5db9..52d97dca 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -6,7 +6,6 @@ use std::{error, fmt, hash}; use bitcoin::bip32::XKeyIdentifier; use bitcoin::hashes::{hash160, ripemd160, sha256, Hash, HashEngine}; -use bitcoin::hex::FromHex; use bitcoin::{self, bip32}; use elements::secp256k1_zkp::{Secp256k1, Signing, Verification}; @@ -276,7 +275,7 @@ impl DescriptorXKey { let xprv = self .xkey .derive_priv(secp, &hardened_path) - .map_err(|_| DescriptorKeyParseError("Unable to derive the hardened steps"))?; + .map_err(DescriptorKeyParseError::DeriveHardenedKey)?; let xpub = bip32::Xpub::from_priv(secp, &xprv); let origin = match &self.origin { @@ -336,7 +335,9 @@ impl DescriptorMultiXKey { if child_num.is_normal() { Ok(*child_num) } else { - Err(DescriptorKeyParseError("Can't make a multi-xpriv with hardened derivation steps that are not shared among all paths into a public key.")) + Err(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::InvalidMultiXKeyDerivation, + )) } }) .collect() @@ -355,7 +356,7 @@ impl DescriptorMultiXKey { let xprv = self .xkey .derive_priv(secp, &hardened_path) - .map_err(|_| DescriptorKeyParseError("Unable to derive the hardened steps"))?; + .map_err(DescriptorKeyParseError::DeriveHardenedKey)?; let xpub = bip32::Xpub::from_priv(secp, &xprv); let origin = match &self.origin { @@ -444,20 +445,149 @@ where } } +/// Kinds of malformed key data +#[derive(Debug, PartialEq, Clone)] +#[non_exhaustive] +#[allow(missing_docs)] +pub enum MalformedKeyDataKind { + EmptyKey, + EncounteredUnprintableCharacter, + InvalidFullPublicKeyPrefix, + InvalidMasterFingerprintLength, + InvalidMultiIndexStep, + InvalidMultiXKeyDerivation, + InvalidPublicKeyLength, + InvalidWildcardInDerivationPath, + KeyTooShort, + MultipleFingerprintsInPublicKey, + MultipleDerivationPathIndexSteps, + NoKeyAfterOrigin, + NoMasterFingerprintFound, + UnclosedSquareBracket, + WildcardAsDerivedDescriptorKey, +} + +impl fmt::Display for MalformedKeyDataKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use MalformedKeyDataKind::*; + + let err = match self { + EmptyKey => "empty key", + EncounteredUnprintableCharacter => "encountered an unprintable character", + InvalidFullPublicKeyPrefix => "only full public keys with prefixes '02', '03' or '04' are allowed", + InvalidMasterFingerprintLength => "master fingerprint should be 8 characters long", + InvalidMultiIndexStep => "invalid multi index step in multipath descriptor", + InvalidMultiXKeyDerivation => "can't make a multi-xpriv with hardened derivation steps that are not shared among all paths into a public key", + InvalidPublicKeyLength => "public keys must be 64, 66 or 130 characters in size", + InvalidWildcardInDerivationPath => "'*' may only appear as last element in a derivation path", + KeyTooShort => "key too short", + MultipleFingerprintsInPublicKey => "multiple ']' in Descriptor Public Key", + MultipleDerivationPathIndexSteps => "'<' may only appear once in a derivation path", + NoKeyAfterOrigin => "no key after origin", + NoMasterFingerprintFound => "no master fingerprint found after '['", + UnclosedSquareBracket => "unclosed '['", + WildcardAsDerivedDescriptorKey => "cannot parse key with a wilcard as a DerivedDescriptorKey", + }; + + f.write_str(err) + } +} + /// Descriptor Key parsing errors -// FIXME: replace with error enums -#[derive(Debug, PartialEq, Clone, Copy)] -pub struct DescriptorKeyParseError(&'static str); +#[derive(Debug, PartialEq, Clone)] +#[non_exhaustive] +pub enum DescriptorKeyParseError { + /// Error while parsing a BIP32 extended private key + Bip32Xpriv(bip32::Error), + /// Error while parsing a BIP32 extended public key + Bip32Xpub(bip32::Error), + /// Error while parsing a derivation index + DerivationIndexError { + /// The invalid index + index: String, + /// The underlying parse error + err: bitcoin::bip32::Error, + }, + /// Error deriving the hardened private key. + DeriveHardenedKey(bip32::Error), + /// Error indicating the key data was malformed + MalformedKeyData(MalformedKeyDataKind), + /// Error while parsing the master derivation path. + MasterDerivationPath(bip32::Error), + /// Error indicating a malformed master fingerprint (invalid hex). + MasterFingerprint { + /// The invalid fingerprint + fingerprint: String, + /// The underlying parse error + err: bitcoin::hex::HexToArrayError, + }, + /// Error while parsing a simple public key. + FullPublicKey(bitcoin::key::ParsePublicKeyError), + /// Error while parsing a WIF private key. + WifPrivateKey(bitcoin::key::FromWifError), + /// Error while parsing an X-only public key (Secp256k1 error). + XonlyPublicKey(bitcoin::secp256k1::Error), + /// Error while parsing a hex private key, the form of a `ct()` view key. Elements only. + HexPrivateKey(bitcoin::secp256k1::Error), +} impl fmt::Display for DescriptorKeyParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.0) + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + DescriptorKeyParseError::Bip32Xpriv(err) => { + write!(f, "error while parsing BIP32 Xpriv: {err}") + } + DescriptorKeyParseError::Bip32Xpub(err) => { + write!(f, "error while parsing BIP32 Xpub: {err}") + } + DescriptorKeyParseError::DerivationIndexError { index, err } => { + write!(f, "error while parsing derivation index '{index}': {err}") + } + DescriptorKeyParseError::DeriveHardenedKey(err) => { + write!(f, "unable to derive the hardened steps: {err}") + } + DescriptorKeyParseError::MalformedKeyData(err) => { + write!(f, "{err}") + } + DescriptorKeyParseError::MasterDerivationPath(err) => { + write!(f, "error while parsing master derivation path: {err}") + } + DescriptorKeyParseError::MasterFingerprint { fingerprint, err } => { + write!(f, "error while parsing master fingerprint '{fingerprint}': {err}") + } + DescriptorKeyParseError::FullPublicKey(err) => { + write!(f, "error while parsing full public key: {err}") + } + DescriptorKeyParseError::WifPrivateKey(err) => { + write!(f, "error while parsing WIF private key: {err}") + } + DescriptorKeyParseError::XonlyPublicKey(err) => { + write!(f, "error while parsing xonly public key: {err}") + } + DescriptorKeyParseError::HexPrivateKey(err) => { + write!(f, "error while parsing hex private key: {err}") + } + } } } impl error::Error for DescriptorKeyParseError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - None + use DescriptorKeyParseError::*; + + match self { + Bip32Xpriv(err) + | Bip32Xpub(err) + | DerivationIndexError { err, .. } + | DeriveHardenedKey(err) + | MasterDerivationPath(err) => Some(err), + MasterFingerprint { err, .. } => Some(err), + FullPublicKey(err) => Some(err), + WifPrivateKey(err) => Some(err), + XonlyPublicKey(err) => Some(err), + HexPrivateKey(err) => Some(err), + MalformedKeyData(_) => None, + } } } @@ -586,15 +716,15 @@ impl FromStr for DescriptorPublicKey { fn from_str(s: &str) -> Result { // A "raw" public key without any origin is the least we accept. if s.len() < 64 { - return Err(DescriptorKeyParseError( - "Key too short (<66 char), doesn't match any format", + return Err(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::KeyTooShort, )); } let (key_part, origin) = parse_key_origin(s)?; if key_part.contains("pub") { - let (xpub, derivation_paths, wildcard) = parse_xkey_deriv::(key_part)?; + let (xpub, derivation_paths, wildcard) = parse_xkey_deriv(parse_bip32_xpub, key_part)?; if derivation_paths.len() > 1 { Ok(DescriptorPublicKey::MultiXPub(DescriptorMultiXKey { origin, @@ -613,10 +743,8 @@ impl FromStr for DescriptorPublicKey { } else { let key = match key_part.len() { 64 => { - let x_only_key = - bitcoin::key::XOnlyPublicKey::from_str(key_part).map_err(|_| { - DescriptorKeyParseError("Error while parsing simple xonly key") - })?; + let x_only_key = bitcoin::key::XOnlyPublicKey::from_str(key_part) + .map_err(DescriptorKeyParseError::XonlyPublicKey)?; SinglePubKey::XOnly(x_only_key) } 66 | 130 => { @@ -624,18 +752,17 @@ impl FromStr for DescriptorPublicKey { || &key_part[0..2] == "03" || &key_part[0..2] == "04") { - return Err(DescriptorKeyParseError( - "Only publickeys with prefixes 02/03/04 are allowed", + return Err(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::InvalidFullPublicKeyPrefix, )); } - let key = bitcoin::PublicKey::from_str(key_part).map_err(|_| { - DescriptorKeyParseError("Error while parsing simple public key") - })?; + let key = bitcoin::PublicKey::from_str(key_part) + .map_err(DescriptorKeyParseError::FullPublicKey)?; SinglePubKey::FullKey(key) } _ => { - return Err(DescriptorKeyParseError( - "Public keys must be 64/66/130 characters in size", + return Err(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::InvalidPublicKeyLength, )) } }; @@ -852,24 +979,24 @@ impl DescriptorSecretKey { let (key_part, origin) = parse_key_origin(s)?; if single_hex && key_part.len() == 64 { - let bytes = Vec::::from_hex(key_part) - .map_err(|_| DescriptorKeyParseError("Error while parsing a HEX private key"))?; + let sk = bitcoin::secp256k1::SecretKey::from_str(key_part) + .map_err(DescriptorKeyParseError::HexPrivateKey)?; let network = bitcoin::network::Network::Bitcoin; // Use any network - let sk = bitcoin::PrivateKey::from_slice(&bytes, network) - .map_err(|_| DescriptorKeyParseError("Error while parsing a HEX private key"))?; + let sk = bitcoin::PrivateKey::new(sk, network); Ok(DescriptorSecretKey::Single(SinglePriv { key: sk, origin: None, })) } else if !single_hex && key_part.len() <= 52 { let sk = bitcoin::PrivateKey::from_str(key_part) - .map_err(|_| DescriptorKeyParseError("Error while parsing a WIF private key"))?; + .map_err(DescriptorKeyParseError::WifPrivateKey)?; Ok(DescriptorSecretKey::Single(SinglePriv { key: sk, origin: None, })) } else { - let (xpriv, derivation_paths, wildcard) = parse_xkey_deriv::(key_part)?; + let (xpriv, derivation_paths, wildcard) = + parse_xkey_deriv(parse_bip32_xpriv, key_part)?; if derivation_paths.len() > 1 { Ok(DescriptorSecretKey::MultiXPrv(DescriptorMultiXKey { origin, @@ -901,47 +1028,56 @@ impl FromStr for DescriptorSecretKey { fn parse_key_origin(s: &str) -> Result<(&str, Option), DescriptorKeyParseError> { for ch in s.as_bytes() { if *ch < 20 || *ch > 127 { - return Err(DescriptorKeyParseError( - "Encountered an unprintable character", + return Err(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::EncounteredUnprintableCharacter, )); } } if s.is_empty() { - return Err(DescriptorKeyParseError("Empty key")); + return Err(DescriptorKeyParseError::MalformedKeyData(MalformedKeyDataKind::EmptyKey)); } let mut parts = s[1..].split(']'); if let Some('[') = s.chars().next() { let mut raw_origin = parts .next() - .ok_or(DescriptorKeyParseError("Unclosed '['"))? + .ok_or(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::UnclosedSquareBracket, + ))? .split('/'); - let origin_id_hex = raw_origin.next().ok_or(DescriptorKeyParseError( - "No master fingerprint found after '['", - ))?; + let origin_id_hex = raw_origin + .next() + .ok_or(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::NoMasterFingerprintFound, + ))?; if origin_id_hex.len() != 8 { - return Err(DescriptorKeyParseError( - "Master fingerprint should be 8 characters long", + return Err(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::InvalidMasterFingerprintLength, )); } - let parent_fingerprint = bip32::Fingerprint::from_str(origin_id_hex).map_err(|_| { - DescriptorKeyParseError("Malformed master fingerprint, expected 8 hex chars") + let parent_fingerprint = bip32::Fingerprint::from_hex(origin_id_hex).map_err(|err| { + DescriptorKeyParseError::MasterFingerprint { + fingerprint: origin_id_hex.to_owned(), + err, + } })?; let origin_path = raw_origin .map(bip32::ChildNumber::from_str) .collect::>() - .map_err(|_| DescriptorKeyParseError("Error while parsing master derivation path"))?; + .map_err(DescriptorKeyParseError::MasterDerivationPath)?; let key = parts .next() - .ok_or(DescriptorKeyParseError("No key after origin."))?; + .ok_or(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::NoKeyAfterOrigin, + ))?; if parts.next().is_some() { - Err(DescriptorKeyParseError( - "Multiple ']' in Descriptor Public Key", + Err(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::MultipleFingerprintsInPublicKey, )) } else { Ok((key, Some((parent_fingerprint, origin_path)))) @@ -951,16 +1087,26 @@ fn parse_key_origin(s: &str) -> Result<(&str, Option), Descrip } } -/// Parse an extended key concatenated to a derivation path. -fn parse_xkey_deriv( +fn parse_bip32_xpub(xkey_str: &str) -> Result { + bip32::Xpub::from_str(xkey_str).map_err(DescriptorKeyParseError::Bip32Xpub) +} + +fn parse_bip32_xpriv(xkey_str: &str) -> Result { + bip32::Xpriv::from_str(xkey_str).map_err(DescriptorKeyParseError::Bip32Xpriv) +} + +fn parse_xkey_deriv( + parse_xkey_fn: impl Fn(&str) -> Result, key_deriv: &str, -) -> Result<(K, Vec, Wildcard), DescriptorKeyParseError> { +) -> Result<(Key, Vec, Wildcard), DescriptorKeyParseError> { let mut key_deriv = key_deriv.split('/'); - let xkey_str = key_deriv.next().ok_or(DescriptorKeyParseError( - "No key found after origin description", - ))?; - let xkey = - K::from_str(xkey_str).map_err(|_| DescriptorKeyParseError("Error while parsing xkey."))?; + let xkey_str = key_deriv + .next() + .ok_or(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::NoKeyAfterOrigin, + ))?; + + let xkey = parse_xkey_fn(xkey_str)?; let mut wildcard = Wildcard::None; let mut multipath = false; @@ -973,8 +1119,8 @@ fn parse_xkey_deriv( wildcard = Wildcard::Hardened; None } else if wildcard != Wildcard::None { - Some(Err(DescriptorKeyParseError( - "'*' may only appear as last element in a derivation path.", + Some(Err(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::InvalidWildcardInDerivationPath, ))) } else { // BIP389 defines a new step in the derivation path. This step contains two or more @@ -982,8 +1128,8 @@ fn parse_xkey_deriv( if p.starts_with('<') && p.ends_with('>') { // There may only be one occurence of this step. if multipath { - return Some(Err(DescriptorKeyParseError( - "'<' may only appear once in a derivation path.", + return Some(Err(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::MultipleDerivationPathIndexSteps, ))); } multipath = true; @@ -991,8 +1137,8 @@ fn parse_xkey_deriv( // The step must contain at least two derivation indexes. // So it's at least '<' + a number + ';' + a number + '>'. if p.len() < 5 || !p.contains(';') { - return Some(Err(DescriptorKeyParseError( - "Invalid multi index step in multipath descriptor.", + return Some(Err(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::InvalidMultiIndexStep, ))); } @@ -1002,10 +1148,11 @@ fn parse_xkey_deriv( indexes .into_iter() .map(|s| { - bip32::ChildNumber::from_str(s).map_err(|_| { - DescriptorKeyParseError( - "Error while parsing index in key derivation path.", - ) + bip32::ChildNumber::from_str(s).map_err(|err| { + DescriptorKeyParseError::DerivationIndexError { + index: s.to_owned(), + err, + } }) }) .collect::, _>>(), @@ -1015,8 +1162,9 @@ fn parse_xkey_deriv( Some( bip32::ChildNumber::from_str(p) .map(|i| vec![i]) - .map_err(|_| { - DescriptorKeyParseError("Error while parsing key derivation path") + .map_err(|err| DescriptorKeyParseError::DerivationIndexError { + index: p.to_owned(), + err, }), ) } @@ -1240,8 +1388,8 @@ impl FromStr for DefiniteDescriptorKey { fn from_str(s: &str) -> Result { let inner = DescriptorPublicKey::from_str(s)?; - DefiniteDescriptorKey::new(inner).ok_or(DescriptorKeyParseError( - "cannot parse key with a wilcard as a DerivedDescriptorKey", + DefiniteDescriptorKey::new(inner).ok_or(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::WildcardAsDerivedDescriptorKey, )) } } @@ -1337,8 +1485,7 @@ mod test { use serde_test::{assert_tokens, Token}; use super::{ - DescriptorKeyParseError, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, - MiniscriptKey, Wildcard, + DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, MiniscriptKey, Wildcard, }; #[test] @@ -1346,62 +1493,50 @@ mod test { // And ones with misplaced wildcard let desc = "[78412e3a/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*/44"; assert_eq!( - DescriptorPublicKey::from_str(desc), - Err(DescriptorKeyParseError( - "\'*\' may only appear as last element in a derivation path." - )) + DescriptorPublicKey::from_str(desc).unwrap_err().to_string(), + "\'*\' may only appear as last element in a derivation path" ); // And ones with invalid fingerprints let desc = "[NonHexor]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*"; assert_eq!( - DescriptorPublicKey::from_str(desc), - Err(DescriptorKeyParseError( - "Malformed master fingerprint, expected 8 hex chars" - )) + DescriptorPublicKey::from_str(desc).unwrap_err().to_string(), + "error while parsing master fingerprint 'NonHexor': failed to parse hex digit" ); // And ones with invalid xpubs.. let desc = "[78412e3a]xpub1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaLcgJvLJuZZvRcEL/1/*"; assert_eq!( - DescriptorPublicKey::from_str(desc), - Err(DescriptorKeyParseError("Error while parsing xkey.")) + DescriptorPublicKey::from_str(desc).unwrap_err().to_string(), + "error while parsing BIP32 Xpub: base58 encoding error" ); // ..or invalid raw keys let desc = "[78412e3a]0208a117f3897c3a13c9384b8695eed98dc31bc2500feb19a1af424cd47a5d83/1/*"; assert_eq!( - DescriptorPublicKey::from_str(desc), - Err(DescriptorKeyParseError( - "Public keys must be 64/66/130 characters in size" - )) + DescriptorPublicKey::from_str(desc).unwrap_err().to_string(), + "public keys must be 64, 66 or 130 characters in size", ); // ..or invalid separators let desc = "[78412e3a]]03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8"; assert_eq!( - DescriptorPublicKey::from_str(desc), - Err(DescriptorKeyParseError( - "Multiple \']\' in Descriptor Public Key" - )) + DescriptorPublicKey::from_str(desc).unwrap_err().to_string(), + "multiple \']\' in Descriptor Public Key" ); // fuzzer errors let desc = "[11111f11]033333333333333333333333333333323333333333333333333333333433333333]]333]]3]]101333333333333433333]]]10]333333mmmm"; assert_eq!( - DescriptorPublicKey::from_str(desc), - Err(DescriptorKeyParseError( - "Multiple \']\' in Descriptor Public Key" - )) + DescriptorPublicKey::from_str(desc).unwrap_err().to_string(), + "multiple \']\' in Descriptor Public Key" ); // fuzz failure, hybrid keys let desc = "0777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777"; assert_eq!( - DescriptorPublicKey::from_str(desc), - Err(DescriptorKeyParseError( - "Only publickeys with prefixes 02/03/04 are allowed" - )) + DescriptorPublicKey::from_str(desc).unwrap_err().to_string(), + "only full public keys with prefixes '02', '03' or '04' are allowed" ); } @@ -1410,26 +1545,24 @@ mod test { // Xpubs are invalid let secret_key = "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"; assert_eq!( - DescriptorSecretKey::from_str(secret_key), - Err(DescriptorKeyParseError("Error while parsing xkey.")) + DescriptorSecretKey::from_str(secret_key) + .unwrap_err() + .to_string(), + "error while parsing BIP32 Xpriv: unknown version magic bytes: [4, 136, 178, 30]" ); // And ones with invalid fingerprints let desc = "[NonHexor]tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/1/*"; assert_eq!( - DescriptorSecretKey::from_str(desc), - Err(DescriptorKeyParseError( - "Malformed master fingerprint, expected 8 hex chars" - )) + DescriptorSecretKey::from_str(desc).unwrap_err().to_string(), + "error while parsing master fingerprint 'NonHexor': failed to parse hex digit" ); // ..or invalid raw keys let desc = "[78412e3a]L32jTfVLei6BYTPUpwpJSkrHx8iL9GZzeErVS8y4Y/1/*"; assert_eq!( - DescriptorSecretKey::from_str(desc), - Err(DescriptorKeyParseError( - "Error while parsing a WIF private key" - )) + DescriptorSecretKey::from_str(desc).unwrap_err().to_string(), + "error while parsing WIF private key: invalid base58" ); } diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 0e254f1e..26077a8d 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -54,7 +54,7 @@ pub(crate) use self::key::maybe_fmt_master_id; pub use self::key::{ ConversionError, DefiniteDescriptorKey, DerivPaths, DescriptorKeyParseError, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, InnerXKey, - SinglePriv, SinglePub, SinglePubKey, Wildcard, + MalformedKeyDataKind, SinglePriv, SinglePub, SinglePubKey, Wildcard, }; pub use self::tr::{TapLeafScript, TapTree, Tr}; /// Alias type for a map of public key to secret key From 8805412c04f9e3d5555e33f1e9cff37877a09a13 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 17:50:55 +0200 Subject: [PATCH 04/18] descriptor: clean up key error matching Port of upstream "descriptor/key.rs: clean up error matching". Source: rust-miniscript bd5db6a Co-authored-by: Andrew Poelstra --- src/descriptor/key.rs | 121 ++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 74 deletions(-) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index 52d97dca..d6e5d3e9 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -469,24 +469,22 @@ pub enum MalformedKeyDataKind { impl fmt::Display for MalformedKeyDataKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use MalformedKeyDataKind::*; - let err = match self { - EmptyKey => "empty key", - EncounteredUnprintableCharacter => "encountered an unprintable character", - InvalidFullPublicKeyPrefix => "only full public keys with prefixes '02', '03' or '04' are allowed", - InvalidMasterFingerprintLength => "master fingerprint should be 8 characters long", - InvalidMultiIndexStep => "invalid multi index step in multipath descriptor", - InvalidMultiXKeyDerivation => "can't make a multi-xpriv with hardened derivation steps that are not shared among all paths into a public key", - InvalidPublicKeyLength => "public keys must be 64, 66 or 130 characters in size", - InvalidWildcardInDerivationPath => "'*' may only appear as last element in a derivation path", - KeyTooShort => "key too short", - MultipleFingerprintsInPublicKey => "multiple ']' in Descriptor Public Key", - MultipleDerivationPathIndexSteps => "'<' may only appear once in a derivation path", - NoKeyAfterOrigin => "no key after origin", - NoMasterFingerprintFound => "no master fingerprint found after '['", - UnclosedSquareBracket => "unclosed '['", - WildcardAsDerivedDescriptorKey => "cannot parse key with a wilcard as a DerivedDescriptorKey", + Self::EmptyKey => "empty key", + Self::EncounteredUnprintableCharacter => "encountered an unprintable character", + Self::InvalidFullPublicKeyPrefix => "only full public keys with prefixes '02', '03' or '04' are allowed", + Self::InvalidMasterFingerprintLength => "master fingerprint should be 8 characters long", + Self::InvalidMultiIndexStep => "invalid multi index step in multipath descriptor", + Self::InvalidMultiXKeyDerivation => "can't make a multi-xpriv with hardened derivation steps that are not shared among all paths into a public key", + Self::InvalidPublicKeyLength => "public keys must be 64, 66 or 130 characters in size", + Self::InvalidWildcardInDerivationPath => "'*' may only appear as last element in a derivation path", + Self::KeyTooShort => "key too short", + Self::MultipleFingerprintsInPublicKey => "multiple ']' in Descriptor Public Key", + Self::MultipleDerivationPathIndexSteps => "'<' may only appear once in a derivation path", + Self::NoKeyAfterOrigin => "no key after origin", + Self::NoMasterFingerprintFound => "no master fingerprint found after '['", + Self::UnclosedSquareBracket => "unclosed '['", + Self::WildcardAsDerivedDescriptorKey => "cannot parse key with a wilcard as a DerivedDescriptorKey", }; f.write_str(err) @@ -534,59 +532,39 @@ pub enum DescriptorKeyParseError { impl fmt::Display for DescriptorKeyParseError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - DescriptorKeyParseError::Bip32Xpriv(err) => { - write!(f, "error while parsing BIP32 Xpriv: {err}") - } - DescriptorKeyParseError::Bip32Xpub(err) => { - write!(f, "error while parsing BIP32 Xpub: {err}") - } - DescriptorKeyParseError::DerivationIndexError { index, err } => { - write!(f, "error while parsing derivation index '{index}': {err}") - } - DescriptorKeyParseError::DeriveHardenedKey(err) => { - write!(f, "unable to derive the hardened steps: {err}") - } - DescriptorKeyParseError::MalformedKeyData(err) => { - write!(f, "{err}") - } - DescriptorKeyParseError::MasterDerivationPath(err) => { - write!(f, "error while parsing master derivation path: {err}") - } - DescriptorKeyParseError::MasterFingerprint { fingerprint, err } => { - write!(f, "error while parsing master fingerprint '{fingerprint}': {err}") - } - DescriptorKeyParseError::FullPublicKey(err) => { - write!(f, "error while parsing full public key: {err}") + Self::Bip32Xpriv(err) => err.fmt(f), + Self::Bip32Xpub(err) => err.fmt(f), + Self::DerivationIndexError { index, err } => { + write!(f, "at derivation index '{index}': {err}") } - DescriptorKeyParseError::WifPrivateKey(err) => { - write!(f, "error while parsing WIF private key: {err}") - } - DescriptorKeyParseError::XonlyPublicKey(err) => { - write!(f, "error while parsing xonly public key: {err}") - } - DescriptorKeyParseError::HexPrivateKey(err) => { - write!(f, "error while parsing hex private key: {err}") + Self::DeriveHardenedKey(err) => err.fmt(f), + Self::MalformedKeyData(err) => err.fmt(f), + Self::MasterDerivationPath(err) => err.fmt(f), + Self::MasterFingerprint { fingerprint, err } => { + write!(f, "on master fingerprint '{fingerprint}': {err}") } + Self::FullPublicKey(err) => err.fmt(f), + Self::WifPrivateKey(err) => err.fmt(f), + Self::XonlyPublicKey(err) => err.fmt(f), + Self::HexPrivateKey(err) => err.fmt(f), } } } impl error::Error for DescriptorKeyParseError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - use DescriptorKeyParseError::*; - match self { - Bip32Xpriv(err) - | Bip32Xpub(err) - | DerivationIndexError { err, .. } - | DeriveHardenedKey(err) - | MasterDerivationPath(err) => Some(err), - MasterFingerprint { err, .. } => Some(err), - FullPublicKey(err) => Some(err), - WifPrivateKey(err) => Some(err), - XonlyPublicKey(err) => Some(err), - HexPrivateKey(err) => Some(err), - MalformedKeyData(_) => None, + Self::Bip32Xpriv(err) + | Self::Bip32Xpub(err) + | Self::DerivationIndexError { err, .. } + | Self::DeriveHardenedKey(err) + | Self::MasterDerivationPath(err) => Some(err), + Self::MasterFingerprint { err, .. } => Some(err), + Self::FullPublicKey(err) => Some(err), + Self::WifPrivateKey(err) => Some(err), + Self::XonlyPublicKey(err) => Some(err), + Self::HexPrivateKey(err) => Some(err), + Self::MalformedKeyData(_) => None, } } } @@ -783,18 +761,16 @@ pub enum ConversionError { impl fmt::Display for ConversionError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match *self { - ConversionError::HardenedChild => "hardened child step in bip32 path", - ConversionError::MultiKey => "multiple existing keys", + Self::HardenedChild => "hardened child step in bip32 path", + Self::MultiKey => "multiple existing keys", }) } } impl error::Error for ConversionError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - use self::ConversionError::*; - match self { - HardenedChild | MultiKey => None, + Self::HardenedChild | Self::MultiKey => None, } } } @@ -1501,14 +1477,14 @@ mod test { let desc = "[NonHexor]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*"; assert_eq!( DescriptorPublicKey::from_str(desc).unwrap_err().to_string(), - "error while parsing master fingerprint 'NonHexor': failed to parse hex digit" + "on master fingerprint 'NonHexor': failed to parse hex digit" ); // And ones with invalid xpubs.. let desc = "[78412e3a]xpub1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaLcgJvLJuZZvRcEL/1/*"; assert_eq!( DescriptorPublicKey::from_str(desc).unwrap_err().to_string(), - "error while parsing BIP32 Xpub: base58 encoding error" + "base58 encoding error" ); // ..or invalid raw keys @@ -1548,22 +1524,19 @@ mod test { DescriptorSecretKey::from_str(secret_key) .unwrap_err() .to_string(), - "error while parsing BIP32 Xpriv: unknown version magic bytes: [4, 136, 178, 30]" + "unknown version magic bytes: [4, 136, 178, 30]" ); // And ones with invalid fingerprints let desc = "[NonHexor]tprv8ZgxMBicQKsPcwcD4gSnMti126ZiETsuX7qwrtMypr6FBwAP65puFn4v6c3jrN9VwtMRMph6nyT63NrfUL4C3nBzPcduzVSuHD7zbX2JKVc/1/*"; assert_eq!( DescriptorSecretKey::from_str(desc).unwrap_err().to_string(), - "error while parsing master fingerprint 'NonHexor': failed to parse hex digit" + "on master fingerprint 'NonHexor': failed to parse hex digit" ); // ..or invalid raw keys let desc = "[78412e3a]L32jTfVLei6BYTPUpwpJSkrHx8iL9GZzeErVS8y4Y/1/*"; - assert_eq!( - DescriptorSecretKey::from_str(desc).unwrap_err().to_string(), - "error while parsing WIF private key: invalid base58" - ); + assert_eq!(DescriptorSecretKey::from_str(desc).unwrap_err().to_string(), "invalid base58"); } #[test] From 045f9e9f6a08707e1cfaf609f3953239405c553c Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 17:51:36 +0200 Subject: [PATCH 05/18] descriptor: add NonDefiniteKeyError for wildcard keys Port of upstream "key.rs: split "definite keys cannot have wildcards" into own error" Source: rust-miniscript 258873f Co-authored-by: Andrew Poelstra --- src/descriptor/key.rs | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index d6e5d3e9..4fba41fd 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -445,6 +445,24 @@ where } } +/// Kinds of malformed key data +#[derive(Debug, PartialEq, Clone)] +#[non_exhaustive] +#[allow(missing_docs)] +pub enum NonDefiniteKeyError { + Wildcard, +} + +impl fmt::Display for NonDefiniteKeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Self::Wildcard => f.write_str("key with a wildcard cannot be a DerivedDescriptorKey"), + } + } +} + +impl error::Error for NonDefiniteKeyError {} + /// Kinds of malformed key data #[derive(Debug, PartialEq, Clone)] #[non_exhaustive] @@ -464,7 +482,6 @@ pub enum MalformedKeyDataKind { NoKeyAfterOrigin, NoMasterFingerprintFound, UnclosedSquareBracket, - WildcardAsDerivedDescriptorKey, } impl fmt::Display for MalformedKeyDataKind { @@ -484,7 +501,6 @@ impl fmt::Display for MalformedKeyDataKind { Self::NoKeyAfterOrigin => "no key after origin", Self::NoMasterFingerprintFound => "no master fingerprint found after '['", Self::UnclosedSquareBracket => "unclosed '['", - Self::WildcardAsDerivedDescriptorKey => "cannot parse key with a wilcard as a DerivedDescriptorKey", }; f.write_str(err) @@ -519,6 +535,8 @@ pub enum DescriptorKeyParseError { /// The underlying parse error err: bitcoin::hex::HexToArrayError, }, + /// Attempt to construct a [`DefiniteDescriptorKey`] from an ambiguous key. + NonDefiniteKey(NonDefiniteKeyError), /// Error while parsing a simple public key. FullPublicKey(bitcoin::key::ParsePublicKeyError), /// Error while parsing a WIF private key. @@ -543,6 +561,7 @@ impl fmt::Display for DescriptorKeyParseError { Self::MasterFingerprint { fingerprint, err } => { write!(f, "on master fingerprint '{fingerprint}': {err}") } + Self::NonDefiniteKey(err) => err.fmt(f), Self::FullPublicKey(err) => err.fmt(f), Self::WifPrivateKey(err) => err.fmt(f), Self::XonlyPublicKey(err) => err.fmt(f), @@ -560,6 +579,7 @@ impl error::Error for DescriptorKeyParseError { | Self::DeriveHardenedKey(err) | Self::MasterDerivationPath(err) => Some(err), Self::MasterFingerprint { err, .. } => Some(err), + Self::NonDefiniteKey(err) => Some(err), Self::FullPublicKey(err) => Some(err), Self::WifPrivateKey(err) => Some(err), Self::XonlyPublicKey(err) => Some(err), @@ -1364,9 +1384,8 @@ impl FromStr for DefiniteDescriptorKey { fn from_str(s: &str) -> Result { let inner = DescriptorPublicKey::from_str(s)?; - DefiniteDescriptorKey::new(inner).ok_or(DescriptorKeyParseError::MalformedKeyData( - MalformedKeyDataKind::WildcardAsDerivedDescriptorKey, - )) + DefiniteDescriptorKey::new(inner) + .ok_or(DescriptorKeyParseError::NonDefiniteKey(NonDefiniteKeyError::Wildcard)) } } From feed6ae552db946eb8d187c7c4bb11f7ed4daf9d Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 17:52:07 +0200 Subject: [PATCH 06/18] descriptor: disallow multipath DefiniteDescriptorKeys Port of upstream "DefiniteDescriptorKey: disallow multipath keys" Elements adaptation: our DefiniteDescriptorKey::new was private and returned Option. Upstream's was already public, so make it public with the upstream signature, Result. Source: rust-miniscript dad3532 Co-authored-by: Andrew Poelstra --- src/descriptor/key.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index 4fba41fd..8d02ee36 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -451,12 +451,14 @@ where #[allow(missing_docs)] pub enum NonDefiniteKeyError { Wildcard, + Multipath, } impl fmt::Display for NonDefiniteKeyError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { Self::Wildcard => f.write_str("key with a wildcard cannot be a DerivedDescriptorKey"), + Self::Multipath => f.write_str("multipath key cannot be a DerivedDescriptorKey"), } } } @@ -1350,11 +1352,13 @@ impl DefiniteDescriptorKey { /// Construct an instance from a descriptor key and a derivation index /// /// Returns `None` if the key contains a wildcard - fn new(key: DescriptorPublicKey) -> Option { + pub fn new(key: DescriptorPublicKey) -> Result { if key.has_wildcard() { - None + Err(NonDefiniteKeyError::Wildcard) + } else if key.is_multipath() { + Err(NonDefiniteKeyError::Multipath) } else { - Some(Self(key)) + Ok(Self(key)) } } @@ -1383,9 +1387,8 @@ impl FromStr for DefiniteDescriptorKey { type Err = DescriptorKeyParseError; fn from_str(s: &str) -> Result { - let inner = DescriptorPublicKey::from_str(s)?; - DefiniteDescriptorKey::new(inner) - .ok_or(DescriptorKeyParseError::NonDefiniteKey(NonDefiniteKeyError::Wildcard)) + let d = DescriptorPublicKey::from_str(s)?; + DefiniteDescriptorKey::new(d).map_err(DescriptorKeyParseError::NonDefiniteKey) } } From 3c8a0bb7a911e5f5e346e6cd474da5b4f9a8821c Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 17:52:37 +0200 Subject: [PATCH 07/18] descriptor: add unit tests for constructing definite keys Port of upstream "descriptor: add unit tests for constructing multipath keys" Source: rust-miniscript b9ccd9d Co-authored-by: Andrew Poelstra --- src/descriptor/key.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index 8d02ee36..e6eacccc 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -1485,6 +1485,8 @@ mod test { use super::{ DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, MiniscriptKey, Wildcard, }; + use crate::descriptor::key::NonDefiniteKeyError; + use crate::DefiniteDescriptorKey; #[test] fn parse_descriptor_key_errors() { @@ -1859,4 +1861,23 @@ mod test { let public_key = DescriptorPublicKey::from_str(desc).unwrap(); assert_tokens(&public_key, &[Token::String(desc)]); } + + #[test] + fn definite_keys() { + // basic xpub + let desc = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8" + .parse::() + .unwrap(); + assert!(matches!(DefiniteDescriptorKey::new(desc), Ok(..))); + // xpub with wildcard + let desc = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/*" + .parse::() + .unwrap(); + assert!(matches!(DefiniteDescriptorKey::new(desc), Err(NonDefiniteKeyError::Wildcard))); + // multipath xpub + let desc = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/<0;1>" + .parse::() + .unwrap(); + assert!(matches!(DefiniteDescriptorKey::new(desc), Err(NonDefiniteKeyError::Multipath))); + } } From 4fe80db367b8c56a1866336df94b51d1eee9f10e Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 17:53:53 +0200 Subject: [PATCH 08/18] descriptor: reject DefiniteDescriptorKeys with hardened steps Port of upstream "key: fix DefiniteDescriptorKey construction from key with hardened step" Adaptations: - Upstream still has `expect` on the constructor result in DescriptorPublicKey::at_derivation_index, so it panics on a hardened step until 97c948d. Here at_derivation_index maps the error to ConversionError::HardenedChild instead, so that no commit in this series panics where the previous one returned an error. The ports of 4930ccb and 97c948d replace the mapping. - Key::to_public_key in the confidential module propagates the at_derivation_index error for bare and view blinding keys instead of expecting success. - descriptor_hardened_wildcard_blinding_key: a ct() descriptor whose bare blinding key has a hardened wildcard now fails in at_derivation_index rather than in address(), so the test asserts that error. Source: rust-miniscript 7b3b3a8 Co-authored-by: Andrew Poelstra --- src/confidential/mod.rs | 29 +++++++++++------------------ src/descriptor/key.rs | 41 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 7e613852..cba774b7 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -81,11 +81,9 @@ impl Key { Err(Error::Unexpected("wildcard blinding key".into())) } else { // Convert into a DefiniteDescriptorKey, note that we are deriving the xpub - // since there is not wildcard. + // since there is not wildcard. This fails for a hardened derivation step. // Consider adding DescriptorPublicKey::to_definite_descriptor - let pk = pk.clone().at_derivation_index(0).expect("single or xpub without wildcards"); - // Derive explicitly rather than relying on ToPublicKey::to_public_key, - // which panics on keys with hardened derivation steps. + let pk = pk.clone().at_derivation_index(0)?; let pk = pk.derive_public_key(secp)?; Ok(bare::tweak_key(secp, spk, &pk)) } @@ -98,7 +96,7 @@ impl Key { if pk.has_wildcard() { Err(Error::Unexpected("wildcard blinding key".into())) } else { - let pk = pk.at_derivation_index(0).expect("single or xprv without wildcards"); + let pk = pk.at_derivation_index(0)?; Ok(bare::tweak_key(secp, spk, &pk)) } } @@ -572,9 +570,8 @@ mod tests { assert!(matches!(desc.key, Key::View(_))); } - // A blinding key with a hardened derivation step cannot be derived from an xpub. This - // must be reported as an error rather than panicking in `ToPublicKey::to_public_key`, - // which unwraps the `ConversionError` returned by `derive_public_key`. + // A blinding key with a hardened derivation step cannot be derived from an xpub. This is + // reported as an error when the blinding key becomes definite. #[test] fn descriptor_hardened_blinding_key() { let secp = secp256k1_zkp::Secp256k1::new(); @@ -597,22 +594,18 @@ mod tests { } // Same case, reached through `at_derivation_index` on a hardened wildcard blinding - // key: the wildcard is replaced by a hardened child, which the xpub cannot derive. + // key: the wildcard is replaced by a hardened child, which the xpub cannot derive, so + // the descriptor does not become definite. #[test] fn descriptor_hardened_wildcard_blinding_key() { - let secp = secp256k1_zkp::Secp256k1::new(); - let params = &elements::AddressParams::LIQUID; - let xpub = "xpub661MyMwAqRbcEcT9W98HZP2kFzyzQQZkYnrRnrM8uD8kH8kSeFoQHq1x2iihLgC6PXGy5LrjCL66uSNhJ8pwjfx2rMUTLWuRMns2EG9xnjs"; let desc_str = format!("ct({}/*h,elwpkh({}/*))", xpub, xpub); let desc = Descriptor::::from_str(&desc_str).unwrap(); - let definite_desc = desc.at_derivation_index(1).unwrap(); - - assert!(matches!( - definite_desc.address(&secp, params).unwrap_err(), - Error::Conversion(ConversionError::HardenedChild) - )); + assert_eq!( + desc.at_derivation_index(1).unwrap_err(), + ConversionError::HardenedChild + ); } #[test] diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index e6eacccc..13f0aee2 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -452,6 +452,7 @@ where pub enum NonDefiniteKeyError { Wildcard, Multipath, + HardenedStep, } impl fmt::Display for NonDefiniteKeyError { @@ -459,6 +460,9 @@ impl fmt::Display for NonDefiniteKeyError { match *self { Self::Wildcard => f.write_str("key with a wildcard cannot be a DerivedDescriptorKey"), Self::Multipath => f.write_str("multipath key cannot be a DerivedDescriptorKey"), + Self::HardenedStep => { + f.write_str("key with hardened derivation steps cannot be a DerivedDescriptorKey") + } } } } @@ -879,6 +883,23 @@ impl DescriptorPublicKey { } } + /// Whether or not the key has a wildcard + pub fn has_hardened_step(&self) -> bool { + let paths = match self { + DescriptorPublicKey::Single(..) => &[], + DescriptorPublicKey::XPub(xpub) => core::slice::from_ref(&xpub.derivation_path), + DescriptorPublicKey::MultiXPub(xpub) => &xpub.derivation_paths.paths()[..], + }; + for p in paths { + for step in p.into_iter() { + if step.is_hardened() { + return true; + } + } + } + false + } + #[deprecated(note = "use at_derivation_index instead")] /// Deprecated name for [`Self::at_derivation_index`]. pub fn derive(self, index: u32) -> Result { @@ -925,8 +946,14 @@ impl DescriptorPublicKey { DescriptorPublicKey::MultiXPub(_) => return Err(ConversionError::MultiKey), }; - Ok(DefiniteDescriptorKey::new(definite) - .expect("The key should not contain any wildcards at this point")) + // Upstream keeps an `expect` here until 97c948d, which panics on a hardened step. Map the + // error instead, since Elements callers derive such keys. + DefiniteDescriptorKey::new(definite).map_err(|e| match e { + NonDefiniteKeyError::HardenedStep => ConversionError::HardenedChild, + NonDefiniteKeyError::Wildcard | NonDefiniteKeyError::Multipath => { + unreachable!("the wildcard is replaced and multipath keys return early") + } + }) } /// Whether or not this key has multiple derivation paths. @@ -1355,6 +1382,8 @@ impl DefiniteDescriptorKey { pub fn new(key: DescriptorPublicKey) -> Result { if key.has_wildcard() { Err(NonDefiniteKeyError::Wildcard) + } else if key.has_hardened_step() { + Err(NonDefiniteKeyError::HardenedStep) } else if key.is_multipath() { Err(NonDefiniteKeyError::Multipath) } else { @@ -1879,5 +1908,13 @@ mod test { .parse::() .unwrap(); assert!(matches!(DefiniteDescriptorKey::new(desc), Err(NonDefiniteKeyError::Multipath))); + // xpub with hardened path + let desc = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/1'/2" + .parse::() + .unwrap(); + assert!(matches!( + DefiniteDescriptorKey::new(desc), + Err(NonDefiniteKeyError::HardenedStep) + )); } } From ef9a76af89fba09a690cb5b640bdfca465ad0bb5 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 17:54:27 +0200 Subject: [PATCH 09/18] descriptor: remove some deprecated methods Port of upstream "key.rs: remove some deprecated methods" Source: rust-miniscript 77ed78b Co-authored-by: Andrew Poelstra --- src/descriptor/key.rs | 12 ------------ src/descriptor/mod.rs | 15 --------------- 2 files changed, 27 deletions(-) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index 13f0aee2..b831f262 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -868,12 +868,6 @@ impl DescriptorPublicKey { } } - /// Whether or not the key has a wildcard - #[deprecated(note = "use has_wildcard instead")] - pub fn is_deriveable(&self) -> bool { - self.has_wildcard() - } - /// Whether or not the key has a wildcard pub fn has_wildcard(&self) -> bool { match *self { @@ -900,12 +894,6 @@ impl DescriptorPublicKey { false } - #[deprecated(note = "use at_derivation_index instead")] - /// Deprecated name for [`Self::at_derivation_index`]. - pub fn derive(self, index: u32) -> Result { - self.at_derivation_index(index) - } - /// Replaces any wildcard (i.e. `/*`) in the key with a particular derivation index, turning it into a /// *definite* key (i.e. one where all the derivation paths are set). /// diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 26077a8d..d39862da 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -808,12 +808,6 @@ impl ForEachKey for Descriptor { } impl Descriptor { - /// Whether or not the descriptor has any wildcards - #[deprecated(note = "use has_wildcards instead")] - pub fn is_deriveable(&self) -> bool { - self.has_wildcard() - } - /// Whether or not the descriptor has any wildcards i.e. `/*`. pub fn has_wildcard(&self) -> bool { self.for_any_key(|key| key.has_wildcard()) @@ -844,15 +838,6 @@ impl Descriptor { self.translate_pk(&mut Derivator(index)) } - #[deprecated(note = "use at_derivation_index instead")] - /// Deprecated name for [`Self::at_derivation_index`]. - pub fn derive( - &self, - index: u32, - ) -> Result, ConversionError> { - self.at_derivation_index(index) - } - /// Convert all the public keys in the descriptor to [`bitcoin::PublicKey`] by deriving them or /// otherwise converting them. All [`bitcoin::key::XOnlyPublicKey`]s are converted to by adding a /// default(0x02) y-coordinate. From 217e5421fbd76816b1c4b8d7aebce5218e1a598b Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 17:56:37 +0200 Subject: [PATCH 10/18] descriptor: make deriving a DefiniteDescriptorKey infallible Port of upstream "key.rs: remove a bunch of unwrap()s which are impossible". Elements adaptations, to callers rather than to the API: - psbt: KeySourceLookUp translates with core::convert::Infallible and expects full_derivation_path, as upstream does. Our Tr and TrExt helper, which upstream does not have, does the same and no longer returns a Result. update_item_with_descriptor_helper keeps its Result, as upstream does, with an allow for clippy::unnecessary_wraps, a lint that our Cargo.toml enables and upstream's does not. - The derived descriptor translators match the Infallible error away, because our translate_pk returns the translator error directly rather than upstream's TranslateErr. - Pegin::derived_descriptor, confidential Key::to_public_key, the xpub_descriptors example and the bitcoind-tests no longer handle an error from these calls. Upstream PR #839 also has e1f0802 ("macros: make target pk and error type optional for translate_* macros") and 6267818 ("TranslateErr: add helper function to unwrap impossible case"). They are not ported. Both build on upstream's Translator trait with associated TargetPk and Error types and on its TranslateErr wrapper, which our fork does not have: our Translator takes the types as parameters, and translate_pk returns E directly. So this commit names all three types in translate_hash_clone!, and matches the Infallible error away where upstream unwraps a TranslateErr. Source: rust-miniscript c169af0 Co-authored-by: Andrew Poelstra --- bitcoind-tests/tests/test_arith.rs | 2 +- bitcoind-tests/tests/test_csfs.rs | 2 +- bitcoind-tests/tests/test_desc.rs | 2 +- bitcoind-tests/tests/test_introspect.rs | 2 +- examples/xpub_descriptors.rs | 1 - src/confidential/mod.rs | 2 +- src/descriptor/key.rs | 19 ++++++-------- src/descriptor/mod.rs | 28 ++++++++++++--------- src/descriptor/pegin/dynafed_pegin.rs | 4 +-- src/psbt/mod.rs | 33 ++++++++++++++----------- 10 files changed, 50 insertions(+), 45 deletions(-) diff --git a/bitcoind-tests/tests/test_arith.rs b/bitcoind-tests/tests/test_arith.rs index c67e844f..8ddae8a4 100644 --- a/bitcoind-tests/tests/test_arith.rs +++ b/bitcoind-tests/tests/test_arith.rs @@ -49,7 +49,7 @@ pub fn test_desc_satisfy(cl: &ElementsD, testdata: &TestData, desc: &str) -> Vec .at_derivation_index(0) .unwrap(); - let derived_desc = definite_desc.derived_descriptor(&secp).unwrap(); + let derived_desc = definite_desc.derived_descriptor(&secp); let desc_address = derived_desc.address(&PARAMS).unwrap(); // No blinding // Next send some btc to each address corresponding to the miniscript diff --git a/bitcoind-tests/tests/test_csfs.rs b/bitcoind-tests/tests/test_csfs.rs index 05bb15be..243d1449 100644 --- a/bitcoind-tests/tests/test_csfs.rs +++ b/bitcoind-tests/tests/test_csfs.rs @@ -50,7 +50,7 @@ pub fn test_desc_satisfy(cl: &ElementsD, testdata: &TestData, desc: &str) -> Vec .at_derivation_index(0) .unwrap(); - let derived_desc = definite_desc.derived_descriptor(&secp).unwrap(); + let derived_desc = definite_desc.derived_descriptor(&secp); let desc_address = derived_desc.address(&PARAMS).unwrap(); // No blinding // Next send some btc to each address corresponding to the miniscript diff --git a/bitcoind-tests/tests/test_desc.rs b/bitcoind-tests/tests/test_desc.rs index 719dbb15..be66d804 100644 --- a/bitcoind-tests/tests/test_desc.rs +++ b/bitcoind-tests/tests/test_desc.rs @@ -86,7 +86,7 @@ pub fn test_desc_satisfy( .at_derivation_index(0) .unwrap(); - let derived_desc = definite_desc.derived_descriptor(&secp).unwrap(); + let derived_desc = definite_desc.derived_descriptor(&secp); let desc_address = derived_desc.address(&PARAMS); // No blinding let desc_address = desc_address.map_err(|_x| DescError::AddressComputationError)?; diff --git a/bitcoind-tests/tests/test_introspect.rs b/bitcoind-tests/tests/test_introspect.rs index e6aee474..4a1544d8 100644 --- a/bitcoind-tests/tests/test_introspect.rs +++ b/bitcoind-tests/tests/test_introspect.rs @@ -49,7 +49,7 @@ pub fn test_desc_satisfy(cl: &ElementsD, testdata: &TestData, desc: &str) -> Vec .at_derivation_index(0) .unwrap(); - let derived_desc = definite_desc.derived_descriptor(&secp).unwrap(); + let derived_desc = definite_desc.derived_descriptor(&secp); let desc_address = derived_desc.address(&PARAMS).unwrap(); // No blinding // Next send some btc to each address corresponding to the miniscript diff --git a/examples/xpub_descriptors.rs b/examples/xpub_descriptors.rs index d0e600d8..7ad1592c 100644 --- a/examples/xpub_descriptors.rs +++ b/examples/xpub_descriptors.rs @@ -46,7 +46,6 @@ fn p2wsh(secp: &Secp256k1) -> Address { let address = Descriptor::::from_str(&s) .unwrap() .derived_descriptor(secp) - .unwrap() .address(&elements::AddressParams::ELEMENTS) .unwrap(); let expected = elements::Address::from_str( diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index cba774b7..4f78eab4 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -84,7 +84,7 @@ impl Key { // since there is not wildcard. This fails for a hardened derivation step. // Consider adding DescriptorPublicKey::to_definite_descriptor let pk = pk.clone().at_derivation_index(0)?; - let pk = pk.derive_public_key(secp)?; + let pk = pk.derive_public_key(secp); Ok(bare::tweak_key(secp, spk, &pk)) } }, diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index b831f262..17aa06f7 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -1337,29 +1337,26 @@ impl DefiniteDescriptorKey { /// avoid this error you should replace any such public keys first with [`translate_pk`]. /// /// [`translate_pk`]: crate::TranslatePk::translate_pk - pub fn derive_public_key( - &self, - secp: &Secp256k1, - ) -> Result { + pub fn derive_public_key(&self, secp: &Secp256k1) -> bitcoin::PublicKey { match self.0 { DescriptorPublicKey::Single(ref pk) => match pk.key { - SinglePubKey::FullKey(pk) => Ok(pk), - SinglePubKey::XOnly(xpk) => Ok(xpk.to_public_key()), + SinglePubKey::FullKey(pk) => pk, + SinglePubKey::XOnly(xpk) => xpk.to_public_key(), }, DescriptorPublicKey::XPub(ref xpk) => match xpk.wildcard { Wildcard::Unhardened | Wildcard::Hardened => { - unreachable!("we've excluded this error case") + unreachable!("impossible by construction of DefiniteDescriptorKey") } Wildcard::None => match xpk.xkey.derive_pub(secp, &xpk.derivation_path.as_ref()) { - Ok(xpub) => Ok(bitcoin::PublicKey::new(xpub.public_key)), + Ok(xpub) => bitcoin::PublicKey::new(xpub.public_key), Err(bip32::Error::CannotDeriveFromHardenedKey) => { - Err(ConversionError::HardenedChild) + unreachable!("impossible by construction of DefiniteDescriptorKey") } Err(e) => unreachable!("cryptographically unreachable: {}", e), }, }, DescriptorPublicKey::MultiXPub(_) => { - unreachable!("A definite key cannot contain a multipath key.") + unreachable!("impossible by construction of DefiniteDescriptorKey") } } } @@ -1437,7 +1434,7 @@ impl MiniscriptKey for DefiniteDescriptorKey { impl ToPublicKey for DefiniteDescriptorKey { fn to_public_key(&self) -> bitcoin::PublicKey { let secp = Secp256k1::verification_only(); - self.derive_public_key(&secp).unwrap() + self.derive_public_key(&secp) } fn to_sha256(hash: &sha256::Hash) -> sha256::Hash { diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index d39862da..5d2d5e0d 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -851,7 +851,7 @@ impl Descriptor { /// .expect("Valid ranged descriptor"); /// # let index = 42; /// # let secp = Secp256k1::verification_only(); - /// let derived_descriptor = descriptor.at_derivation_index(index).unwrap().derived_descriptor(&secp).unwrap(); + /// let derived_descriptor = descriptor.at_derivation_index(index).unwrap().derived_descriptor(&secp); /// # assert_eq!(descriptor.derived_descriptor(&secp, index).unwrap(), derived_descriptor); /// ``` /// @@ -869,7 +869,7 @@ impl Descriptor { secp: &secp256k1_zkp::Secp256k1, index: u32, ) -> Result, ConversionError> { - self.at_derivation_index(index)?.derived_descriptor(secp) + Ok(self.at_derivation_index(index)?.derived_descriptor(secp)) } /// Parse a descriptor that may contain secret keys @@ -1132,7 +1132,7 @@ impl Descriptor { /// let secp = secp256k1::Secp256k1::verification_only(); /// let descriptor = Descriptor::::from_str("eltr(xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ/0/*)") /// .expect("Valid ranged descriptor"); - /// let result = descriptor.at_derivation_index(0).unwrap().derived_descriptor(&secp).expect("Non-hardened derivation"); + /// let result = descriptor.at_derivation_index(0).unwrap().derived_descriptor(&secp); /// assert_eq!(result.to_string(), "eltr(03cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115)#hr5pt2wj"); /// ``` /// @@ -1142,25 +1142,31 @@ impl Descriptor { pub fn derived_descriptor( &self, secp: &secp256k1::Secp256k1, - ) -> Result, ConversionError> { + ) -> Descriptor { struct Derivator<'a, C: secp256k1::Verification>(&'a secp256k1::Secp256k1); impl<'a, C: secp256k1::Verification> - Translator + Translator for Derivator<'a, C> { fn pk( &mut self, pk: &DefiniteDescriptorKey, - ) -> Result { - pk.derive_public_key(self.0) + ) -> Result { + Ok(pk.derive_public_key(self.0)) } - translate_hash_clone!(DefiniteDescriptorKey, bitcoin::PublicKey, ConversionError); + translate_hash_clone!( + DefiniteDescriptorKey, + bitcoin::PublicKey, + core::convert::Infallible + ); } - let derived = self.translate_pk(&mut Derivator(secp))?; - Ok(derived) + match self.translate_pk(&mut Derivator(secp)) { + Ok(derived) => derived, + Err(e) => match e {}, + } } } @@ -1997,14 +2003,12 @@ mod tests { .at_derivation_index(index) .unwrap() .derived_descriptor(&secp_ctx) - .unwrap() .address(&elements::AddressParams::ELEMENTS) .unwrap(); let addr_two = desc_two .at_derivation_index(index) .unwrap() .derived_descriptor(&secp_ctx) - .unwrap() .address(&elements::AddressParams::ELEMENTS) .unwrap(); let addr_expected = elements::Address::from_str(raw_addr_expected).unwrap(); diff --git a/src/descriptor/pegin/dynafed_pegin.rs b/src/descriptor/pegin/dynafed_pegin.rs index 68911caf..0495e94e 100644 --- a/src/descriptor/pegin/dynafed_pegin.rs +++ b/src/descriptor/pegin/dynafed_pegin.rs @@ -69,7 +69,7 @@ impl Pegin { secp: &secp256k1_zkp::Secp256k1, ) -> Result, Error> { let elem_desc = self.elem_desc.at_derivation_index(arg)?; - let elem_desc = elem_desc.derived_descriptor(secp)?; + let elem_desc = elem_desc.derived_descriptor(secp); Ok(Pegin::new(self.fed_desc.clone(), elem_desc)) } } @@ -386,7 +386,7 @@ mod tests { let elem_desc = "ct(slip77(ab5824f4477b4ebb00a132adfd8eb0b7935cf24f6ac151add5d1913db374ce92),elwpkh([759db348/84'/1'/0']tpubDCRMaF33e44pcJj534LXVhFbHibPbJ5vuLhSSPFAw57kYURv4tzXFL6LSnd78bkjqdmE3USedkbpXJUPA1tdzKfuYSL7PianceqAhwL2UkA/0/*))"; let elem_desc: ConfidentialDescriptor = elem_desc.parse().unwrap(); let elem_desc = elem_desc.descriptor.at_derivation_index(0).unwrap(); - let elem_desc = elem_desc.derived_descriptor(&secp).unwrap(); + let elem_desc = elem_desc.derived_descriptor(&secp); let pegin = Pegin::new(d.clone(), elem_desc); assert_eq!(pegin.to_string(), "pegin(wsh(or_d(multi(11,020e0338c96a8870479f2396c373cc7696ba124e8635d41b0ea581112b67817261,02675333a4e4b8fb51d9d4e22fa5a8eaced3fdac8a8cbf9be8c030f75712e6af99,02896807d54bc55c24981f24a453c60ad3e8993d693732288068a23df3d9f50d48,029e51a5ef5db3137051de8323b001749932f2ff0d34c82e96a2c2461de96ae56c,02a4e1a9638d46923272c266631d94d36bdb03a64ee0e14c7518e49d2f29bc4010,031c41fdbcebe17bec8d49816e00ca1b5ac34766b91c9f2ac37d39c63e5e008afb,03079e252e85abffd3c401a69b087e590a9b86f33f574f08129ccbd3521ecf516b,03111cf405b627e22135b3b3733a4a34aa5723fb0f58379a16d32861bf576b0ec2,0318f331b3e5d38156da6633b31929c5b220349859cc9ca3d33fb4e68aa0840174,03230dae6b4ac93480aeab26d000841298e3b8f6157028e47b0897c1e025165de1,035abff4281ff00660f99ab27bb53e6b33689c2cd8dcd364bc3c90ca5aea0d71a6,03bd45cddfacf2083b14310ae4a84e25de61e451637346325222747b157446614c,03cc297026b06c71cbfa52089149157b5ff23de027ac5ab781800a578192d17546,03d3bde5d63bdb3a6379b461be64dad45eabff42f758543a9645afd42f6d424828,03ed1e8d5109c9ed66f7941bc53cc71137baa76d50d274bda8d5e8ffbd6e61fe9a),and_v(v:older(4032),multi(2,03aab896d53a8e7d6433137bbba940f9c521e085dd07e60994579b64a6d992cf79,0291b7d0b1b692f8f524516ed950872e5da10fb1b808b5a526dedc6fed1cf29807,0386aa9372fbab374593466bc5451dc59954e90787f08060964d95c87ef34ca5bb)))),elwpkh(0321da398ca2ddc09be89caa26e6730ae84751b6ea3a1ca46aa365bb5e1c3d9620))#qp4fan9q"); diff --git a/src/psbt/mod.rs b/src/psbt/mod.rs index 9fad0318..17bb8dfa 100644 --- a/src/psbt/mod.rs +++ b/src/psbt/mod.rs @@ -1063,20 +1063,20 @@ struct KeySourceLookUp( pub secp256k1::Secp256k1, ); -impl Translator +impl Translator for KeySourceLookUp { fn pk( &mut self, xpk: &DefiniteDescriptorKey, - ) -> Result { - let derived = xpk.derive_public_key(&self.1)?; + ) -> Result { + let derived = xpk.derive_public_key(&self.1); self.0.insert( derived.to_public_key(), ( xpk.master_fingerprint(), xpk.full_derivation_path() - .ok_or(descriptor::ConversionError::MultiKey)?, + .expect("definite keys cannot be multikeys"), ), ); Ok(derived) @@ -1085,7 +1085,7 @@ impl Translator( item: &mut F, descriptor: &Descriptor>, @@ -1198,7 +1201,7 @@ fn update_item_with_descriptor_helper( let secp = secp256k1::Secp256k1::verification_only(); let derived = if let Descriptor::Tr(_) = &descriptor { - let derived = descriptor.derived_descriptor(&secp)?; + let derived = descriptor.derived_descriptor(&secp); if let Some(check_script) = check_script { if check_script != &derived.script_pubkey() { @@ -1210,13 +1213,13 @@ fn update_item_with_descriptor_helper( // NOTE: they will both always be Tr if let (Descriptor::Tr(tr_derived), Descriptor::Tr(tr_xpk)) = (&derived, descriptor) { - update_tr_psbt_helper(item, tr_derived, tr_xpk)?; + update_tr_psbt_helper(item, tr_derived, tr_xpk); } derived } else if let Descriptor::TrExt(_) = &descriptor { // Repeat the same code for Tr with extensions. Annoying to dedup this code without macros - let derived = descriptor.derived_descriptor(&secp)?; + let derived = descriptor.derived_descriptor(&secp); if let Some(check_script) = check_script { if check_script != &derived.script_pubkey() { @@ -1226,13 +1229,16 @@ fn update_item_with_descriptor_helper( // NOTE: they will both always be Tr if let (Descriptor::TrExt(tr_derived), Descriptor::TrExt(tr_xpk)) = (&derived, descriptor) { - update_tr_psbt_helper(item, tr_derived, tr_xpk)?; + update_tr_psbt_helper(item, tr_derived, tr_xpk); } derived } else { let mut bip32_derivation = KeySourceLookUp(BTreeMap::new(), Secp256k1::verification_only()); - let derived = descriptor.translate_pk(&mut bip32_derivation)?; + let derived = match descriptor.translate_pk(&mut bip32_derivation) { + Ok(derived) => derived, + Err(e) => match e {}, + }; if let Some(check_script) = check_script { if check_script != &derived.script_pubkey() { @@ -1272,7 +1278,7 @@ fn update_tr_psbt_helper( item: &mut F, tr_derived: &Tr, tr_xpk: &Tr, -) -> Result<(), descriptor::ConversionError> +) where Ext: ParseableExt, Ext2: Extension, @@ -1292,7 +1298,7 @@ where ik_xpk.master_fingerprint(), ik_xpk .full_derivation_path() - .ok_or(descriptor::ConversionError::MultiKey)?, + .expect("definite keys cannot be multikeys"), ), ), ); @@ -1331,7 +1337,7 @@ where ( xpk.master_fingerprint(), xpk.full_derivation_path() - .ok_or(descriptor::ConversionError::MultiKey)?, + .expect("definite keys cannot be multikeys"), ), ) }); @@ -1354,7 +1360,6 @@ where } _ => {} } - Ok(()) } // Get a script from witness script pubkey hash From e20c496e17ce9abd426e947cf268b26c5f6d9b7a Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 17:57:12 +0200 Subject: [PATCH 11/18] psbt: remove derives from UtxoUpdateError and OutputUpdateError Port of upstream "psbt: remove derives from UtxoUpdateError and OutputUpdateError". Source: rust-miniscript 627d963 Co-authored-by: Andrew Poelstra --- src/descriptor/key.rs | 6 +++--- src/psbt/mod.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index 17aa06f7..d6a359c9 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -446,7 +446,7 @@ where } /// Kinds of malformed key data -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] #[allow(missing_docs)] pub enum NonDefiniteKeyError { @@ -470,7 +470,7 @@ impl fmt::Display for NonDefiniteKeyError { impl error::Error for NonDefiniteKeyError {} /// Kinds of malformed key data -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] #[allow(missing_docs)] pub enum MalformedKeyDataKind { @@ -514,7 +514,7 @@ impl fmt::Display for MalformedKeyDataKind { } /// Descriptor Key parsing errors -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub enum DescriptorKeyParseError { /// Error while parsing a BIP32 extended private key diff --git a/src/psbt/mod.rs b/src/psbt/mod.rs index 17bb8dfa..5ad4265c 100644 --- a/src/psbt/mod.rs +++ b/src/psbt/mod.rs @@ -1374,7 +1374,7 @@ fn script_code_wpkh(script: &Script) -> Script { } /// Return error type for [`PsbtExt::update_input_with_descriptor`] -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum UtxoUpdateError { /// Index out of bounds IndexOutOfBounds(usize, usize), @@ -1422,7 +1422,7 @@ impl error::Error for UtxoUpdateError { } /// Return error type for [`PsbtExt::update_output_with_descriptor`] -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum OutputUpdateError { /// Index out of bounds IndexOutOfBounds(usize, usize), From 1d711cae1b3fc1e01f010f584947c22e427e4001 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 17:58:32 +0200 Subject: [PATCH 12/18] descriptor: replace ConversionError with NonDefiniteKeyError Port of upstream "key: replace ConversionError with NonDefiniteKeyError". Elements adaptations, to callers of the API: - Error::Conversion(ConversionError) becomes Error::NonDefiniteKey(NonDefiniteKeyError). Upstream's Error has no such variant, but ours is how confidential and pegin code report a key that cannot be derived. - confidential::Descriptor::at_derivation_index returns NonDefiniteKeyError: HardenedStep for a hardened index on an xprv view key and Multipath for a multipath view key. - DescriptorPublicKey::at_derivation_index returns the result of DefiniteDescriptorKey::new directly, where upstream still expects it until 97c948d. The previous commits mapped that result to ConversionError instead of expecting it, so this keeps the series free of new panics. Source: rust-miniscript 4930ccb Co-authored-by: Andrew Poelstra --- src/confidential/mod.rs | 14 ++++++------- src/descriptor/key.rs | 46 +++++++---------------------------------- src/descriptor/mod.rs | 20 ++++++++++-------- src/lib.rs | 14 ++++++------- src/psbt/mod.rs | 14 ++++++------- 5 files changed, 40 insertions(+), 68 deletions(-) diff --git a/src/confidential/mod.rs b/src/confidential/mod.rs index 4f78eab4..b72a4612 100644 --- a/src/confidential/mod.rs +++ b/src/confidential/mod.rs @@ -28,7 +28,7 @@ use elements::secp256k1_zkp; use crate::descriptor::checksum::{self, verify_checksum}; use crate::descriptor::{ - ConversionError, DefiniteDescriptorKey, DescriptorSecretKey, DescriptorPublicKey, + DefiniteDescriptorKey, DescriptorSecretKey, DescriptorPublicKey, NonDefiniteKeyError, DescriptorXKey, Wildcard }; use crate::expression::FromTree; @@ -128,7 +128,7 @@ impl Descriptor { /// /// # Errors /// - If index ≥ 2^31 - pub fn at_derivation_index(&self, index: u32) -> Result, ConversionError> { + pub fn at_derivation_index(&self, index: u32) -> Result, NonDefiniteKeyError> { let definite_key = match self.key.clone() { Key::Slip77(k) => Key::Slip77(k), Key::Bare(k) => Key::Bare(k.at_derivation_index(index)?.into_descriptor_public_key()), @@ -141,12 +141,12 @@ impl Descriptor { Wildcard::Unhardened => xprv.derivation_path.into_child( bip32::ChildNumber::from_normal_idx(index) .ok() - .ok_or(ConversionError::HardenedChild)?, + .ok_or(NonDefiniteKeyError::HardenedStep)?, ), Wildcard::Hardened => xprv.derivation_path.into_child( bip32::ChildNumber::from_hardened_idx(index) .ok() - .ok_or(ConversionError::HardenedChild)?, + .ok_or(NonDefiniteKeyError::HardenedStep)?, ), }; DescriptorSecretKey::XPrv(DescriptorXKey { @@ -156,7 +156,7 @@ impl Descriptor { wildcard: Wildcard::None, }) }, - DescriptorSecretKey::MultiXPrv(_) => return Err(ConversionError::MultiKey), + DescriptorSecretKey::MultiXPrv(_) => return Err(NonDefiniteKeyError::Multipath), }), }; let definite_descriptor = self.descriptor.at_derivation_index(index)?; @@ -589,7 +589,7 @@ mod tests { assert!(matches!( desc.address(&secp, params).unwrap_err(), - Error::Conversion(ConversionError::HardenedChild) + Error::NonDefiniteKey(NonDefiniteKeyError::HardenedStep) )); } @@ -604,7 +604,7 @@ mod tests { let desc = Descriptor::::from_str(&desc_str).unwrap(); assert_eq!( desc.at_derivation_index(1).unwrap_err(), - ConversionError::HardenedChild + NonDefiniteKeyError::HardenedStep ); } diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index d6a359c9..5721919e 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -775,32 +775,6 @@ impl FromStr for DescriptorPublicKey { } } -/// Descriptor key conversion error -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum ConversionError { - /// Attempted to convert a key with hardened derivations to a bitcoin public key - HardenedChild, - /// Attempted to convert a key with multiple derivation paths to a bitcoin public key - MultiKey, -} - -impl fmt::Display for ConversionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match *self { - Self::HardenedChild => "hardened child step in bip32 path", - Self::MultiKey => "multiple existing keys", - }) - } -} - -impl error::Error for ConversionError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::HardenedChild | Self::MultiKey => None, - } - } -} - impl DescriptorPublicKey { /// The fingerprint of the master key associated with this key, `0x00000000` if none. pub fn master_fingerprint(&self) -> bip32::Fingerprint { @@ -907,7 +881,10 @@ impl DescriptorPublicKey { /// /// - If `index` is hardened. /// - If the key contains multi-path derivations - pub fn at_derivation_index(self, index: u32) -> Result { + pub fn at_derivation_index( + self, + index: u32, + ) -> Result { let definite = match self { DescriptorPublicKey::Single(_) => self, DescriptorPublicKey::XPub(xpub) => { @@ -916,12 +893,12 @@ impl DescriptorPublicKey { Wildcard::Unhardened => xpub.derivation_path.into_child( bip32::ChildNumber::from_normal_idx(index) .ok() - .ok_or(ConversionError::HardenedChild)?, + .ok_or(NonDefiniteKeyError::HardenedStep)?, ), Wildcard::Hardened => xpub.derivation_path.into_child( bip32::ChildNumber::from_hardened_idx(index) .ok() - .ok_or(ConversionError::HardenedChild)?, + .ok_or(NonDefiniteKeyError::HardenedStep)?, ), }; DescriptorPublicKey::XPub(DescriptorXKey { @@ -931,17 +908,10 @@ impl DescriptorPublicKey { wildcard: Wildcard::None, }) } - DescriptorPublicKey::MultiXPub(_) => return Err(ConversionError::MultiKey), + DescriptorPublicKey::MultiXPub(_) => return Err(NonDefiniteKeyError::Multipath), }; - // Upstream keeps an `expect` here until 97c948d, which panics on a hardened step. Map the - // error instead, since Elements callers derive such keys. - DefiniteDescriptorKey::new(definite).map_err(|e| match e { - NonDefiniteKeyError::HardenedStep => ConversionError::HardenedChild, - NonDefiniteKeyError::Wildcard | NonDefiniteKeyError::Multipath => { - unreachable!("the wildcard is replaced and multipath keys return early") - } - }) + DefiniteDescriptorKey::new(definite) } /// Whether or not this key has multiple derivation paths. diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 5d2d5e0d..c5913a0a 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -52,9 +52,9 @@ mod key; pub use self::csfs_cov::{CovError, CovOperations, LegacyCSFSCov, LegacyCovSatisfier}; pub(crate) use self::key::maybe_fmt_master_id; pub use self::key::{ - ConversionError, DefiniteDescriptorKey, DerivPaths, DescriptorKeyParseError, - DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, InnerXKey, - MalformedKeyDataKind, SinglePriv, SinglePub, SinglePubKey, Wildcard, + DefiniteDescriptorKey, DerivPaths, DescriptorKeyParseError, DescriptorMultiXKey, + DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, InnerXKey, MalformedKeyDataKind, + NonDefiniteKeyError, SinglePriv, SinglePub, SinglePubKey, Wildcard, }; pub use self::tr::{TapLeafScript, TapTree, Tr}; /// Alias type for a map of public key to secret key @@ -822,18 +822,20 @@ impl Descriptor { pub fn at_derivation_index( &self, index: u32, - ) -> Result, ConversionError> { + ) -> Result, NonDefiniteKeyError> { struct Derivator(u32); - impl Translator for Derivator { + impl Translator + for Derivator + { fn pk( &mut self, pk: &DescriptorPublicKey, - ) -> Result { + ) -> Result { pk.clone().at_derivation_index(self.0) } - translate_hash_clone!(DescriptorPublicKey, DescriptorPublicKey, ConversionError); + translate_hash_clone!(DescriptorPublicKey, DescriptorPublicKey, NonDefiniteKeyError); } self.translate_pk(&mut Derivator(index)) } @@ -868,7 +870,7 @@ impl Descriptor { &self, secp: &secp256k1_zkp::Secp256k1, index: u32, - ) -> Result, ConversionError> { + ) -> Result, NonDefiniteKeyError> { Ok(self.at_derivation_index(index)?.derived_descriptor(secp)) } @@ -1065,7 +1067,7 @@ impl Descriptor { secp: &secp256k1_zkp::Secp256k1, script_pubkey: &Script, range: Range, - ) -> Result)>, ConversionError> { + ) -> Result)>, NonDefiniteKeyError> { let range = if self.has_wildcard() { range } else { 0..1 }; for i in range { diff --git a/src/lib.rs b/src/lib.rs index d76928e4..b4948cd1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -367,8 +367,8 @@ pub enum Error { /// The federation descriptor has a type not supported UnsupportedAddressForPegin, - /// Conversion error in descriptor - Conversion(descriptor::ConversionError), + /// A descriptor key cannot become a [`descriptor::DefiniteDescriptorKey`] + NonDefiniteKey(descriptor::NonDefiniteKeyError), } #[doc(hidden)] @@ -453,9 +453,9 @@ impl From for Error { } } -impl From for Error { - fn from(e: descriptor::ConversionError) -> Error { - Error::Conversion(e) +impl From for Error { + fn from(e: descriptor::NonDefiniteKeyError) -> Error { + Error::NonDefiniteKey(e) } } @@ -533,7 +533,7 @@ impl fmt::Display for Error { Error::MultiATooManyKeys(k) => write!(f, "MultiA too many keys {}", k), Error::TrNoScriptCode => write!(f, "No script code for Tr descriptors"), Error::MultipathDescLenMismatch => write!(f, "At least two BIP389 key expressions in the descriptor contain tuples of derivation indexes of different lengths"), - Error::Conversion(ref e) => e.fmt(f), + Error::NonDefiniteKey(ref e) => e.fmt(f), Error::UnsupportedAddressForPegin => write!(f, "Cannot create the address from the pegin descriptor, the federation descriptor is of an unsuppported type"), } @@ -586,7 +586,7 @@ impl error::Error for Error { ContextError(e) => Some(e), AnalysisError(e) => Some(e), PubKeyCtxError(e, _) => Some(e), - Conversion(e) => Some(e), + NonDefiniteKey(e) => Some(e), } } } diff --git a/src/psbt/mod.rs b/src/psbt/mod.rs index 5ad4265c..fd3a5e1d 100644 --- a/src/psbt/mod.rs +++ b/src/psbt/mod.rs @@ -1008,14 +1008,14 @@ pub trait PsbtInputExt { fn update_with_descriptor_unchecked( &mut self, descriptor: &Descriptor>, - ) -> Result>, descriptor::ConversionError>; + ) -> Result>, descriptor::NonDefiniteKeyError>; } impl PsbtInputExt for psbt::Input { fn update_with_descriptor_unchecked( &mut self, descriptor: &Descriptor>, - ) -> Result>, descriptor::ConversionError> + ) -> Result>, descriptor::NonDefiniteKeyError> { let (derived, _) = update_item_with_descriptor_helper(self, descriptor, None)?; Ok(derived) @@ -1043,14 +1043,14 @@ pub trait PsbtOutputExt { fn update_with_descriptor_unchecked( &mut self, descriptor: &Descriptor, - ) -> Result, descriptor::ConversionError>; + ) -> Result, descriptor::NonDefiniteKeyError>; } impl PsbtOutputExt for psbt::Output { fn update_with_descriptor_unchecked( &mut self, descriptor: &Descriptor, - ) -> Result, descriptor::ConversionError> { + ) -> Result, descriptor::NonDefiniteKeyError> { let (derived, _) = update_item_with_descriptor_helper(self, descriptor, None)?; Ok(derived) } @@ -1196,7 +1196,7 @@ fn update_item_with_descriptor_helper( Descriptor>, bool, ), - descriptor::ConversionError, + descriptor::NonDefiniteKeyError, > { let secp = secp256k1::Secp256k1::verification_only(); @@ -1381,7 +1381,7 @@ pub enum UtxoUpdateError { /// The unsigned transaction didn't have an input at that index MissingInputUtxo, /// Derivation error - DerivationError(descriptor::ConversionError), + DerivationError(descriptor::NonDefiniteKeyError), /// The PSBT's `witness_utxo` and/or `non_witness_utxo` were invalid or missing UtxoCheck, /// The PSBT's `witness_utxo` and/or `non_witness_utxo` had a script_pubkey that did not match @@ -1429,7 +1429,7 @@ pub enum OutputUpdateError { /// The raw unsigned transaction didn't have an output at that index MissingTxOut, /// Derivation error - DerivationError(descriptor::ConversionError), + DerivationError(descriptor::NonDefiniteKeyError), /// The output's script_pubkey did not match the descriptor MismatchedScriptPubkey, } From 890e9aa72cfe6316289908c92909f105d227cf8c Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 19:41:55 +0200 Subject: [PATCH 13/18] fuzz: add fuzz test for parsing definite descriptor keys Port of upstream "fuzz: add fuzz test for parsing definite descriptor keys". Adaptations: the target imports elements_miniscript as miniscript, as the other targets do, and computes the address with AddressParams::ELEMENTS instead of Network::Bitcoin. Upstream's workflow file is cron-daily-fuzz.yml. Ours is fuzz.yml. Source: rust-miniscript f09bf48 Co-authored-by: Andrew Poelstra --- .github/workflows/fuzz.yml | 1 + fuzz/Cargo.toml | 4 ++++ .../fuzz_targets/parse_descriptor_definite.rs | 23 +++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 fuzz/fuzz_targets/parse_descriptor_definite.rs diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 0f21f20c..d0a6e76a 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -25,6 +25,7 @@ roundtrip_descriptor, roundtrip_concrete, compile_descriptor, roundtrip_confidential, +parse_descriptor_definite, ] steps: - name: Install test dependencies diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 4f09e76e..cd6feb4c 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -48,3 +48,7 @@ path = "fuzz_targets/compile_descriptor.rs" [[bin]] name = "roundtrip_confidential" path = "fuzz_targets/roundtrip_confidential.rs" + +[[bin]] +name = "parse_descriptor_definite" +path = "fuzz_targets/parse_descriptor_definite.rs" diff --git a/fuzz/fuzz_targets/parse_descriptor_definite.rs b/fuzz/fuzz_targets/parse_descriptor_definite.rs new file mode 100644 index 00000000..886ff458 --- /dev/null +++ b/fuzz/fuzz_targets/parse_descriptor_definite.rs @@ -0,0 +1,23 @@ +#![allow(unexpected_cfgs)] + +extern crate elements_miniscript as miniscript; + +use honggfuzz::fuzz; +use miniscript::{DefiniteDescriptorKey, Descriptor}; + +fn do_test(data: &[u8]) { + let data_str = String::from_utf8_lossy(data); + + if let Ok(desc) = data_str.parse::>() { + let _ = desc.to_string(); + let _ = desc.address(&miniscript::elements::AddressParams::ELEMENTS); + } +} + +fn main() { + loop { + fuzz!(|data| { + do_test(data); + }); + } +} From 3b017d1537332c2611ed38b3fa81937ecd606d9f Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 17:58:47 +0200 Subject: [PATCH 14/18] descriptor: fix the doc line of has_hardened_step Partial port of upstream "feat: add new functions on DescriptorPublicKey" (doc line only) Source: rust-miniscript 4906bed (partial: doc line only) Co-authored-by: Trevor Arjeski --- src/descriptor/key.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index 5721919e..0fc9144f 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -851,7 +851,7 @@ impl DescriptorPublicKey { } } - /// Whether or not the key has a wildcard + /// Whether or not the key has a hardened step in path pub fn has_hardened_step(&self) -> bool { let paths = match self { DescriptorPublicKey::Single(..) => &[], From 7d1e3d71de1bc1c4dbf69b39513ddde2a26bf369 Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 18:00:04 +0200 Subject: [PATCH 15/18] descriptor: document the hardened step error of at_derivation_index Port of upstream "Fix hardened derivation path error handling in `at_derivation_index`". Source: rust-miniscript 97c948d Co-authored-by: Velnbur --- src/descriptor/key.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index 0fc9144f..c75ca57c 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -880,6 +880,7 @@ impl DescriptorPublicKey { /// # Errors /// /// - If `index` is hardened. + /// - If derivation path contains a hardened step. /// - If the key contains multi-path derivations pub fn at_derivation_index( self, From 39cea4e35d139d1a12180ccdb231e02ded6d390e Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 18:01:22 +0200 Subject: [PATCH 16/18] descriptor: reject definite keys deeper than BIP32 allows Port of upstream "descriptor: reject definite keys deeper than BIP32 allows". Adaptations, because our fork lacks upstream 1062144, which added Descriptor::into_definite and deprecated at_derivation_index: - The last check of the definite_key_maximum_depth test calls at_derivation_index(0) where upstream calls into_definite(). Both go through DefiniteDescriptorKey::new, so they check the same thing. The descriptor is built with Descriptor::::new_tr. - The depth line that upstream adds to the into_definite and derive_at_index docs goes on our Descriptor::at_derivation_index. Source: rust-miniscript 613ed0d Co-authored-by: Boris Nagaev --- src/descriptor/key.rs | 98 +++++++++++++++++++++++++++++++++++++++++-- src/descriptor/mod.rs | 5 ++- 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index c75ca57c..8725fd5a 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -453,6 +453,7 @@ pub enum NonDefiniteKeyError { Wildcard, Multipath, HardenedStep, + MaximumDepthExceeded, } impl fmt::Display for NonDefiniteKeyError { @@ -463,6 +464,9 @@ impl fmt::Display for NonDefiniteKeyError { Self::HardenedStep => { f.write_str("key with hardened derivation steps cannot be a DerivedDescriptorKey") } + Self::MaximumDepthExceeded => f.write_str( + "key deeper than BIP32's maximum depth of 255 cannot be a DerivedDescriptorKey", + ), } } } @@ -868,6 +872,24 @@ impl DescriptorPublicKey { false } + /// Whether deriving this key, including a wildcard step, would go deeper than BIP32's maximum + /// depth of 255. + fn exceeds_max_depth(&self) -> bool { + match self { + Self::Single(..) => false, + Self::XPub(xpub) => derivation_exceeds_max_depth( + xpub.xkey.depth, + core::slice::from_ref(&xpub.derivation_path), + xpub.wildcard, + ), + Self::MultiXPub(xpub) => derivation_exceeds_max_depth( + xpub.xkey.depth, + xpub.derivation_paths.paths(), + xpub.wildcard, + ), + } + } + /// Replaces any wildcard (i.e. `/*`) in the key with a particular derivation index, turning it into a /// *definite* key (i.e. one where all the derivation paths are set). /// @@ -882,6 +904,7 @@ impl DescriptorPublicKey { /// - If `index` is hardened. /// - If derivation path contains a hardened step. /// - If the key contains multi-path derivations + /// - If deriving the key would go deeper than BIP32's maximum depth of 255 pub fn at_derivation_index( self, index: u32, @@ -1071,6 +1094,20 @@ fn parse_key_origin(s: &str) -> Result<(&str, Option), Descrip } } +/// Whether deriving from an extended key at `depth` along any of `paths`, plus one more step for a +/// wildcard, would go deeper than BIP32's maximum depth of 255. +/// +/// The paths hold only the steps after the extended key. Its depth already includes the steps of +/// its origin, so those are not counted again. +fn derivation_exceeds_max_depth( + depth: u8, + paths: &[bip32::DerivationPath], + wildcard: Wildcard, +) -> bool { + let longest = paths.iter().map(|path| path.len()).max().unwrap_or(0); + longest + usize::from(wildcard != Wildcard::None) > usize::from(u8::MAX - depth) +} + fn parse_bip32_xpub(xkey_str: &str) -> Result { bip32::Xpub::from_str(xkey_str).map_err(DescriptorKeyParseError::Bip32Xpub) } @@ -1332,9 +1369,10 @@ impl DefiniteDescriptorKey { } } - /// Construct an instance from a descriptor key and a derivation index + /// Constructs an instance from a descriptor key. /// - /// Returns `None` if the key contains a wildcard + /// Returns an error if the key has a wildcard, a hardened derivation step or several derivation + /// paths, or if deriving it would go deeper than BIP32's maximum depth of 255. pub fn new(key: DescriptorPublicKey) -> Result { if key.has_wildcard() { Err(NonDefiniteKeyError::Wildcard) @@ -1342,6 +1380,8 @@ impl DefiniteDescriptorKey { Err(NonDefiniteKeyError::HardenedStep) } else if key.is_multipath() { Err(NonDefiniteKeyError::Multipath) + } else if key.exceeds_max_depth() { + Err(NonDefiniteKeyError::MaximumDepthExceeded) } else { Ok(Self(key)) } @@ -1468,10 +1508,11 @@ mod test { use serde_test::{assert_tokens, Token}; use super::{ - DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, MiniscriptKey, Wildcard, + DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, + MiniscriptKey, Wildcard, }; use crate::descriptor::key::NonDefiniteKeyError; - use crate::DefiniteDescriptorKey; + use crate::{DefiniteDescriptorKey, Descriptor}; #[test] fn parse_descriptor_key_errors() { @@ -1873,4 +1914,53 @@ mod test { Err(NonDefiniteKeyError::HardenedStep) )); } + + #[test] + fn definite_key_maximum_depth() { + let key = |depth: u8, steps: usize, wildcard: Wildcard| { + let mut xkey = bip32::Xpub::from_str("xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8").unwrap(); + xkey.depth = depth; + DescriptorPublicKey::XPub(DescriptorXKey { + origin: None, + xkey, + derivation_path: vec![bip32::ChildNumber::from(0); steps].into(), + wildcard, + }) + }; + let new = |key| DefiniteDescriptorKey::new(key).map(|_| ()); + let too_deep = Err(NonDefiniteKeyError::MaximumDepthExceeded); + + // BIP32 depth stops at 255, so an xpub at `depth` can take 255 - depth more steps. + assert_eq!(new(key(0, 255, Wildcard::None)), Ok(())); + assert_eq!(new(key(0, 256, Wildcard::None)), too_deep); + assert_eq!(new(key(200, 55, Wildcard::None)), Ok(())); + assert_eq!(new(key(200, 56, Wildcard::None)), too_deep); + + // Deriving at an index turns the wildcard into one more step. + assert!(key(200, 54, Wildcard::Unhardened) + .at_derivation_index(7) + .is_ok()); + assert_eq!( + key(200, 55, Wildcard::Unhardened) + .at_derivation_index(7) + .map(|_| ()), + too_deep + ); + + // Origin steps don't count, since the xpub's depth already includes them. + let mut key_with_origin = key(0, 255, Wildcard::None); + if let DescriptorPublicKey::XPub(ref mut xpub) = key_with_origin { + xpub.origin = Some(( + bip32::Fingerprint::default(), + vec![bip32::ChildNumber::from(0); 300].into(), + )); + } + assert_eq!(new(key_with_origin), Ok(())); + + // A descriptor with such a key doesn't become definite, so it can't panic later when + // deriving its script pubkey. + let desc = Descriptor::::new_tr(key(0, 256, Wildcard::None), None) + .unwrap(); + assert_eq!(desc.at_derivation_index(0).map(|_| ()), too_deep); + } } diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index c5913a0a..afa1a6c7 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -819,6 +819,7 @@ impl Descriptor { /// # Errors /// - If index ≥ 2^31 /// - If the descriptor contains multi-path derivations + /// - If deriving a key would go deeper than BIP32's maximum depth of 255 pub fn at_derivation_index( &self, index: u32, @@ -864,8 +865,8 @@ impl Descriptor { /// /// # Errors /// - /// This function will return an error for multi-path descriptors - /// or if hardened derivation is attempted, + /// This function will return an error for multi-path descriptors, if hardened derivation is + /// attempted, or if deriving a key would go deeper than BIP32's maximum depth of 255. pub fn derived_descriptor( &self, secp: &secp256k1_zkp::Secp256k1, From 2d5f25b75570b5c77aee969071b8377de83b5b9a Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 18:02:05 +0200 Subject: [PATCH 17/18] descriptor: reject keys deeper than BIP32 allows when parsing Port of upstream "descriptor: reject keys deeper than BIP32 allows when parsing". Adaptations: - Our xprv check sits in DescriptorSecretKey::from_str_inner, so it also covers the xprv view key of a ct() descriptor. - The test descriptor is eltr() instead of tr(). Source: rust-miniscript f2862f7 Co-authored-by: Boris Nagaev --- src/descriptor/key.rs | 54 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/descriptor/key.rs b/src/descriptor/key.rs index 8725fd5a..9dcf6e57 100644 --- a/src/descriptor/key.rs +++ b/src/descriptor/key.rs @@ -487,6 +487,7 @@ pub enum MalformedKeyDataKind { InvalidPublicKeyLength, InvalidWildcardInDerivationPath, KeyTooShort, + MaximumDepthExceeded, MultipleFingerprintsInPublicKey, MultipleDerivationPathIndexSteps, NoKeyAfterOrigin, @@ -506,6 +507,9 @@ impl fmt::Display for MalformedKeyDataKind { Self::InvalidPublicKeyLength => "public keys must be 64, 66 or 130 characters in size", Self::InvalidWildcardInDerivationPath => "'*' may only appear as last element in a derivation path", Self::KeyTooShort => "key too short", + Self::MaximumDepthExceeded => { + "key derivation would go deeper than BIP32's maximum depth of 255" + } Self::MultipleFingerprintsInPublicKey => "multiple ']' in Descriptor Public Key", Self::MultipleDerivationPathIndexSteps => "'<' may only appear once in a derivation path", Self::NoKeyAfterOrigin => "no key after origin", @@ -733,6 +737,11 @@ impl FromStr for DescriptorPublicKey { if key_part.contains("pub") { let (xpub, derivation_paths, wildcard) = parse_xkey_deriv(parse_bip32_xpub, key_part)?; + if derivation_exceeds_max_depth(xpub.depth, &derivation_paths, wildcard) { + return Err(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::MaximumDepthExceeded, + )); + } if derivation_paths.len() > 1 { Ok(DescriptorPublicKey::MultiXPub(DescriptorMultiXKey { origin, @@ -1004,6 +1013,11 @@ impl DescriptorSecretKey { } else { let (xpriv, derivation_paths, wildcard) = parse_xkey_deriv(parse_bip32_xpriv, key_part)?; + if derivation_exceeds_max_depth(xpriv.depth, &derivation_paths, wildcard) { + return Err(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::MaximumDepthExceeded, + )); + } if derivation_paths.len() > 1 { Ok(DescriptorSecretKey::MultiXPrv(DescriptorMultiXKey { origin, @@ -1508,8 +1522,8 @@ mod test { use serde_test::{assert_tokens, Token}; use super::{ - DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, - MiniscriptKey, Wildcard, + DescriptorKeyParseError, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, + DescriptorXKey, MalformedKeyDataKind, MiniscriptKey, Wildcard, }; use crate::descriptor::key::NonDefiniteKeyError; use crate::{DefiniteDescriptorKey, Descriptor}; @@ -1963,4 +1977,40 @@ mod test { .unwrap(); assert_eq!(desc.at_derivation_index(0).map(|_| ()), too_deep); } + + #[test] + fn parse_key_maximum_depth() { + // BIP32 test vector 1 master keys, at depth 0. + let xpub = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"; + let xprv = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi"; + let steps = |n: usize| "/0".repeat(n); + let public = |s: String| DescriptorPublicKey::from_str(&s).map(|_| ()); + let secret = |s: String| DescriptorSecretKey::from_str(&s).map(|_| ()); + let too_deep = Err(DescriptorKeyParseError::MalformedKeyData( + MalformedKeyDataKind::MaximumDepthExceeded, + )); + + // BIP32 depth stops at 255. A wildcard or a multipath step counts as one more step. + assert_eq!(public(format!("{xpub}{}", steps(255))), Ok(())); + assert_eq!(public(format!("{xpub}{}", steps(256))), too_deep); + assert_eq!(public(format!("{xpub}{}/*", steps(254))), Ok(())); + assert_eq!(public(format!("{xpub}{}/*", steps(255))), too_deep); + assert_eq!(public(format!("{xpub}{}/<0;1>", steps(254))), Ok(())); + assert_eq!(public(format!("{xpub}{}/<0;1>", steps(255))), too_deep); + assert_eq!(secret(format!("{xprv}{}/*", steps(254))), Ok(())); + assert_eq!(secret(format!("{xprv}{}/*", steps(255))), too_deep); + + // An xpub at depth 200 can take 55 more steps. + let mut xkey = bip32::Xpub::from_str(xpub).unwrap(); + xkey.depth = 200; + assert_eq!(public(format!("{xkey}{}", steps(55))), Ok(())); + assert_eq!(public(format!("{xkey}{}", steps(56))), too_deep); + + // Origin steps don't count, since the xpub's depth already includes them. + assert_eq!(public(format!("[d34db33f{}]{xpub}{}", steps(300), steps(255))), Ok(())); + + // A descriptor with such a key fails to parse, before anything can be derived from it. + let desc = format!("eltr({xpub}{})", steps(256)); + assert!(Descriptor::::from_str(&desc).is_err()); + } } From 07aefaf7e832803dd9d2e4ff160db3e46fc4e9dc Mon Sep 17 00:00:00 2001 From: Kyryl R Date: Tue, 10 Nov 2026 19:42:17 +0200 Subject: [PATCH 18/18] CHANGELOG: describe the definite key and key error changes --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 629200b8..c71ba460 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,15 @@ - `ct()` descriptors accept `eltr` with script leaves that contain no extension fragments. - Miniscript and policy strings accept a trailing descriptor checksum, which is verified. The text after the last `#` is read as the checksum, so a key containing `#` is only accepted when a valid checksum follows. - `pegin()` and `legacy_pegin()` descriptors can be parsed from strings. `LegacyPegin` displays its inner descriptor without the inner checksum. A successful parse does not check the restrictions that pegin spending places on the descriptor. Unsupported forms, such as an `eltr` Elements descriptor, still parse and can panic in the unchanged Bitcoin-side methods, including `bitcoin_address`, `bitcoin_witness_script` and `get_bitcoin_satisfaction`. +- Breaking: `DescriptorKeyParseError` is a `#[non_exhaustive]` enum instead of a struct holding a string, and no longer derives `Copy`. The new `MalformedKeyDataKind` enum describes malformed key data. Messages start with a lowercase letter and follow rust-miniscript. The Elements-only `HexPrivateKey` variant reports a bad hex view key in a `ct()` descriptor. Ported from rust-miniscript 497c5a3 and bd5db6a. +- Breaking: `ConversionError` is replaced by the `#[non_exhaustive]` `NonDefiniteKeyError`, with `Wildcard`, `Multipath`, `HardenedStep` and `MaximumDepthExceeded` variants. It is returned by `DefiniteDescriptorKey::new`, `DescriptorPublicKey::at_derivation_index`, `Descriptor::at_derivation_index`, `Descriptor::derived_descriptor`, `Descriptor::find_derivation_index_for_spk`, `confidential::Descriptor::at_derivation_index` and the PSET `update_with_descriptor_unchecked` methods, and held by `UtxoUpdateError::DerivationError` and `OutputUpdateError::DerivationError`. `Error::Conversion` becomes `Error::NonDefiniteKey`. Ported from rust-miniscript 258873f, dad3532, 7b3b3a8, 4930ccb and 613ed0d. +- Breaking: `DefiniteDescriptorKey::new` is public and returns `Result`. Keys with a wildcard were already rejected. It now also rejects a hardened derivation step after an xpub (`HardenedStep`), several derivation paths (`Multipath`), and a derivation deeper than BIP32's maximum depth of 255 (`MaximumDepthExceeded`), checked in the order wildcard, hardened step, multipath, depth. `DescriptorPublicKey::at_derivation_index`, and the descriptor methods built on it, return the same errors. `DefiniteDescriptorKey::from_str` wraps them in `DescriptorKeyParseError::NonDefiniteKey`, so `Descriptor::::from_str` now rejects hardened and multipath keys, which previously parsed and panicked when derived, for example in `script_pubkey`. A `ct()` descriptor whose bare blinding key has a hardened wildcard now fails in `at_derivation_index` instead of `address`. Ported from rust-miniscript dad3532, 7b3b3a8, 97c948d and 613ed0d. +- Parsing an xpub or xprv whose derivation goes deeper than BIP32's maximum depth of 255 fails with `DescriptorKeyParseError::MalformedKeyData(MalformedKeyDataKind::MaximumDepthExceeded)`, so such a string never reaches the constructor. A wildcard or multipath step counts as one step, and origin steps don't count. The constructor's `NonDefiniteKeyError::MaximumDepthExceeded` is only returned for keys built in code. Ported from rust-miniscript f2862f7. +- Breaking: `DefiniteDescriptorKey::derive_public_key` returns a `bitcoin::PublicKey` and `Descriptor::::derived_descriptor` returns a descriptor, instead of a `Result`. Ported from rust-miniscript c169af0. +- Breaking: the deprecated `DescriptorPublicKey::is_deriveable`, `DescriptorPublicKey::derive`, `Descriptor::is_deriveable` and `Descriptor::derive` are removed. Use `has_wildcard` and `at_derivation_index`. Ported from rust-miniscript 77ed78b. +- Breaking: `UtxoUpdateError` and `OutputUpdateError` derive only `Debug`, `PartialEq`, `Eq` and `Clone`. `NonDefiniteKeyError`, `MalformedKeyDataKind` and `DescriptorKeyParseError` derive `Eq`. Ported from rust-miniscript 627d963. +- Add `DescriptorPublicKey::has_hardened_step`. Ported from rust-miniscript 7b3b3a8 and, for its doc line, 4906bed (partial). +- `DescriptorSecretKey::to_public` converts a multi-xpriv into a multi-xpub when all hardened derivation steps are shared among its paths, instead of always failing. Ported from rust-miniscript 7d3e1e8. # 0.5.0 - Sep 20, 2026