Skip to content

descriptor: port upstream definite key and key error changes - #117

Open
KyrylR wants to merge 18 commits into
ElementsProject:masterfrom
KyrylR:pr/107-upstream-key-errors
Open

KyrylR wants to merge 18 commits into
ElementsProject:masterfrom
KyrylR:pr/107-upstream-key-errors

Conversation

@KyrylR

@KyrylR KyrylR commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Created this PR with a help of GPT Astra, Opus 5.5, and Mythos 5.1

All commits except the one for the CHANGELOG are cherry-picked from the https://git.rust-bitcoin.org/rust-bitcoin/rust-miniscript/ (all of them has Source: rust-miniscript and the related author)

This work intends to simplify future forward port of elements-miniscript into rust-miniscript and current work on porting the taproot into the elements-miniscript project

Part of #107. Next step of the migration in #108: this brings our key code back in line with https://git.rust-bitcoin.org/rust-bitcoin/rust-miniscript/ master (d57bb31)


Ported commits (AI helped with creating a summary below)

Upstream PR Subject
3d9acd2 #752 document an error case for at_derivation_index
7d3e1e8 #757 convert multi-xprivs into multi-xpubs
497c5a3 #804 make DescriptorKeyParseError an enum
bd5db6a #830 clean up key error matching
258873f #830 add NonDefiniteKeyError for wildcard keys
dad3532 #830 disallow multipath definite keys
b9ccd9d #830 unit tests for constructing definite keys
7b3b3a8 #839 reject definite keys with hardened steps
77ed78b #839 remove deprecated is_deriveable/derive
c169af0 #839 make deriving a definite key infallible
627d963 #839 remove derives from the PSBT update errors
4930ccb #839 replace ConversionError with NonDefiniteKeyError
f09bf48 #839 fuzz test for parsing definite keys
4906bed (doc line only) #898 doc of has_hardened_step
97c948d #913 document the hardened-step error of at_derivation_index
613ed0d #1065 reject definite keys deeper than BIP32 allows
f2862f7 #1065 reject such keys when parsing

Not ported:

  • ae64ce6 (#757): formatting only.
  • e1f0802 and 6267818 (#839): they need upstream's associated-type Translator and TranslateErr. 6267818 is ported in the follow-up translate PR.
  • f5a6857 (#898): we already have Display for Wildcard.
  • 15e3465 and 945adb7 (#898), and the rest of 4906bed: BIP-388 wallet policies and new accessors, unrelated to definite keys.
  • 1062144 and a15ce35 (#911, #946): into_definite, derive_at_index and NoWildcard. This would move every Elements caller of at_derivation_index, so it belongs in its own PR. Tests that call into_definite() upstream use at_derivation_index(0) here, which goes through the same DefiniteDescriptorKey::new.

Breaking changes

  • ConversionError is replaced by NonDefiniteKeyError (Wildcard, Multipath, HardenedStep, MaximumDepthExceeded). The error type changes for:
    • at_derivation_index (descriptor, key and confidential::Descriptor)
    • derived_descriptor(secp, index)
    • find_derivation_index_for_spk
    • the PSET update methods
    • Error::Conversion, which is now Error::NonDefiniteKey
  • DefiniteDescriptorKey::new is public. Hardened, multipath and over-deep keys can no longer become definite, and DefiniteDescriptorKey::from_str rejects them.
  • DescriptorKeyParseError is a #[non_exhaustive] enum, no longer Copy, with lowercase messages as upstream. It has one Elements-only variant, HexPrivateKey, for ct() hex view keys.
  • derive_public_key and Descriptor<DefiniteDescriptorKey>::derived_descriptor return values, not Results.
  • The deprecated is_deriveable and derive are removed.
  • UtxoUpdateError and OutputUpdateError derive only Debug, PartialEq, Eq, Clone.
  • A ct() descriptor whose bare blinding key has a hardened wildcard now fails in at_derivation_index instead of address.
Remaining differences from upstream master

None of these is introduced by this PR. They come from upstream commits outside its scope, or from Elements-only code.

  • Pre-existing
    • No into_definite / derive_at_index / NoWildcard (1062144, a15ce35).
    • No XKeyParseError or UnexpectedXPrivateKey. Public-key parsing dispatches on contains("pub") rather than the key prefix (15e3465, 945adb7, d7deb69).
    • WIF and hex secret keys drop a parsed [fingerprint/path] origin (9416098 not ported).
    • Missing accessors: full_derivation_paths, derivation_path(s), wildcard, xkey_network, DerivPaths::single, and From<PublicKey>/From<XOnlyPublicKey> for DescriptorPublicKey.
    • No no_std support (std::error instead of core and the prelude). Self:: paths are not used (7063e4a).
    • The Wildcard::Hardened doc says "Unhardened wildcard".
  • Elements-only
    • DescriptorKeyParseError::HexPrivateKey.
    • Error::NonDefiniteKey.
    • secp256k1-zkp types.
    • derived_descriptor ends with Err(e) => match e {} on Infallible, since our translate_pk has no TranslateErr yet.
    • The PSET helper keeps its Result with #[allow(clippy::unnecessary_wraps)], because our Cargo.toml enables that lint.
    • The fuzz target uses elements_miniscript and AddressParams::ELEMENTS.
  • Matching upstream, reported upstream
    • The derive_public_key and derived_descriptor docs still describe errors these functions can no longer return.
    • NonDefiniteKeyError is documented as "Kinds of malformed key data", and its messages say "DerivedDescriptorKey".
    • fmt_derivation_paths panics on a multipath key built in code with paths of different lengths.
    • parse_xkey_deriv expands multipath alternatives before the depth check.
    • into_single_descriptors takes the path count from the first multipath key.

Testing

  • Every commit: cargo test --all-features, cargo check --workspace --all-targets --all-features and nightly-2025-08-19 clippy with -D warnings pass. Library tests go from 139 to 143.
  • At the tip:
    • all CI feature combinations, nightly, and MSRV 1.74.0
    • cargo doc -D warnings
    • the 7 CI examples and wasm32
    • bitcoind-tests and the fuzz crate, including the new target, compile
  • Not run: the bitcoind-tests integration run and honggfuzz.
  • rustfmt: cargo +nightly-2025-08-19 fmt --all -- --check reports 48 hunks, against 39 on master. Master isn't fmt-clean and CI has no fmt job.

KyrylR and others added 18 commits November 11, 2026 12:06
Source: rust-miniscript 3d9acd2
Co-authored-by: Nadav Ivgi <nadav@shesek.info>
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 <nadav@shesek.info>
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 <chris_ricketts@protonmail.com>
Port of upstream "descriptor/key.rs: clean up error matching".

Source: rust-miniscript bd5db6a
Co-authored-by: Andrew Poelstra <apoelstra@wpsoftware.net>
Port of upstream "key.rs: split "definite keys cannot have wildcards" into own error"

Source: rust-miniscript 258873f
Co-authored-by: Andrew Poelstra <apoelstra@wpsoftware.net>
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<Self, NonDefiniteKeyError>.

Source: rust-miniscript dad3532
Co-authored-by: Andrew Poelstra <apoelstra@wpsoftware.net>
Port of upstream "descriptor: add unit tests for constructing multipath keys"

Source: rust-miniscript b9ccd9d
Co-authored-by: Andrew Poelstra <apoelstra@wpsoftware.net>
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 <apoelstra@wpsoftware.net>
Port of upstream "key.rs: remove some deprecated methods"

Source: rust-miniscript 77ed78b
Co-authored-by: Andrew Poelstra <apoelstra@wpsoftware.net>
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<P, Q, E> 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 <apoelstra@wpsoftware.net>
Port of upstream "psbt: remove derives from UtxoUpdateError and OutputUpdateError".

Source: rust-miniscript 627d963
Co-authored-by: Andrew Poelstra <apoelstra@wpsoftware.net>
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 <apoelstra@wpsoftware.net>
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 <apoelstra@wpsoftware.net>
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 <tmarjeski@gmail.com>
Port of upstream "Fix hardened derivation path error handling in `at_derivation_index`".

Source: rust-miniscript 97c948d
Co-authored-by: Velnbur <kyrylobaybula@gmail.com>
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::<DescriptorPublicKey>::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 <bnagaev@gmail.com>
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 <bnagaev@gmail.com>
Comment thread CHANGELOG.md
Comment on lines +16 to 25
- 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.

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

@KyrylR

KyrylR commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

If/when this merged I will be able to fully resolve the #107

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant