Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
22ee2f0
descriptor: document an error case for at_derivation_index
KyrylR Nov 10, 2026
4ab5fc6
descriptor: support conversion of multi-xprivs into multi-xpubs
KyrylR Nov 10, 2026
d61f3c9
descriptor: make DescriptorKeyParseError an enum
KyrylR Nov 10, 2026
8805412
descriptor: clean up key error matching
KyrylR Nov 10, 2026
045f9e9
descriptor: add NonDefiniteKeyError for wildcard keys
KyrylR Nov 10, 2026
feed6ae
descriptor: disallow multipath DefiniteDescriptorKeys
KyrylR Nov 10, 2026
3c8a0bb
descriptor: add unit tests for constructing definite keys
KyrylR Nov 10, 2026
4fe80db
descriptor: reject DefiniteDescriptorKeys with hardened steps
KyrylR Nov 10, 2026
ef9a76a
descriptor: remove some deprecated methods
KyrylR Nov 10, 2026
217e542
descriptor: make deriving a DefiniteDescriptorKey infallible
KyrylR Nov 10, 2026
e20c496
psbt: remove derives from UtxoUpdateError and OutputUpdateError
KyrylR Nov 10, 2026
1d711ca
descriptor: replace ConversionError with NonDefiniteKeyError
KyrylR Nov 10, 2026
890e9aa
fuzz: add fuzz test for parsing definite descriptor keys
KyrylR Nov 10, 2026
3b017d1
descriptor: fix the doc line of has_hardened_step
KyrylR Nov 10, 2026
7d1e3d7
descriptor: document the hardened step error of at_derivation_index
KyrylR Nov 10, 2026
39cea4e
descriptor: reject definite keys deeper than BIP32 allows
KyrylR Nov 10, 2026
2d5f25b
descriptor: reject keys deeper than BIP32 allows when parsing
KyrylR Nov 10, 2026
07aefaf
CHANGELOG: describe the definite key and key error changes
KyrylR Nov 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ roundtrip_descriptor,
roundtrip_concrete,
compile_descriptor,
roundtrip_confidential,
parse_descriptor_definite,
]
steps:
- name: Install test dependencies
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, NonDefiniteKeyError>`. 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::<DefiniteDescriptorKey>::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::<DefiniteDescriptorKey>::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.

Comment on lines +16 to 25

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no way around with this changelog

Maybe the reorg will help, but the total number of already introduced changes does not help

Will be grateful for the advice regarding this

# 0.5.0 - Sep 20, 2026

Expand Down
2 changes: 1 addition & 1 deletion bitcoind-tests/tests/test_arith.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bitcoind-tests/tests/test_csfs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bitcoind-tests/tests/test_desc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;

Expand Down
2 changes: 1 addition & 1 deletion bitcoind-tests/tests/test_introspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion examples/xpub_descriptors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ fn p2wsh<C: Verification>(secp: &Secp256k1<C>) -> Address {
let address = Descriptor::<DefiniteDescriptorKey>::from_str(&s)
.unwrap()
.derived_descriptor(secp)
.unwrap()
.address(&elements::AddressParams::ELEMENTS)
.unwrap();
let expected = elements::Address::from_str(
Expand Down
4 changes: 4 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
23 changes: 23 additions & 0 deletions fuzz/fuzz_targets/parse_descriptor_definite.rs
Original file line number Diff line number Diff line change
@@ -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::<Descriptor<DefiniteDescriptorKey>>() {
let _ = desc.to_string();
let _ = desc.address(&miniscript::elements::AddressParams::ELEMENTS);
}
}

fn main() {
loop {
fuzz!(|data| {
do_test(data);
});
}
}
45 changes: 19 additions & 26 deletions src/confidential/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -81,12 +81,10 @@ 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.derive_public_key(secp)?;
let pk = pk.clone().at_derivation_index(0)?;
let pk = pk.derive_public_key(secp);
Ok(bare::tweak_key(secp, spk, &pk))
}
},
Expand All @@ -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))
}
}
Expand Down Expand Up @@ -130,7 +128,7 @@ impl<T: Extension + ParseableExt> Descriptor<DescriptorPublicKey, T> {
///
/// # Errors
/// - If index ≥ 2^31
pub fn at_derivation_index(&self, index: u32) -> Result<Descriptor<DefiniteDescriptorKey, T>, ConversionError> {
pub fn at_derivation_index(&self, index: u32) -> Result<Descriptor<DefiniteDescriptorKey, T>, 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()),
Expand All @@ -143,12 +141,12 @@ impl<T: Extension + ParseableExt> Descriptor<DescriptorPublicKey, T> {
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 {
Expand All @@ -158,7 +156,7 @@ impl<T: Extension + ParseableExt> Descriptor<DescriptorPublicKey, T> {
wildcard: Wildcard::None,
})
},
DescriptorSecretKey::MultiXPrv(_) => return Err(ConversionError::MultiKey),
DescriptorSecretKey::MultiXPrv(_) => return Err(NonDefiniteKeyError::Multipath),
}),
};
let definite_descriptor = self.descriptor.at_derivation_index(index)?;
Expand Down Expand Up @@ -487,7 +485,7 @@ mod tests {
),
(
"ct(L3jXxwef3fpB7hcrFozcWgHeJCPSAFiZ1Ji2YJMPxceaGvy3PC1q,elwpkh(03774eec7a3d550d18e9f89414152025b3b0ad6a342b19481f702d843cff06dfc4))#gcy6hcfz",
"unexpected «Key too short (<66 char), doesn't match any format»",
"unexpected «key too short»",
),
];

Expand Down Expand Up @@ -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();
Expand All @@ -592,27 +589,23 @@ mod tests {

assert!(matches!(
desc.address(&secp, params).unwrap_err(),
Error::Conversion(ConversionError::HardenedChild)
Error::NonDefiniteKey(NonDefiniteKeyError::HardenedStep)
));
}

// 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::<DescriptorPublicKey, NoExt>::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(),
NonDefiniteKeyError::HardenedStep
);
}

#[test]
Expand Down
Loading
Loading