Skip to main content

md_codec/
to_miniscript.rs

1//! v0.32 AST → `miniscript::Descriptor<DescriptorPublicKey>` converter.
2//!
3//! Replaces the v0.14-era hand-rolled 5-shape allow-list with a generic
4//! converter that builds a miniscript `Descriptor` from any
5//! BIP-388-parseable md1 wire AST. Address derivation
6//! ([`crate::Descriptor::derive_address`]) delegates to this module then
7//! to `miniscript::Descriptor::address`.
8//!
9//! Feature-gated behind `derive` (default-on).
10
11use crate::canonicalize::{ExpandedKey, expand_per_at_n};
12use crate::derive::xpub_from_tlv_bytes;
13use crate::encode::Descriptor;
14use crate::error::Error;
15use crate::origin_path::OriginPath;
16use crate::tag::Tag;
17use crate::tree::{Body, Node};
18use crate::use_site_path::UseSitePath;
19
20use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint};
21use miniscript::descriptor::{
22    DerivPaths, DescriptorMultiXKey, DescriptorPublicKey, DescriptorXKey, SinglePub, SinglePubKey,
23    Wildcard,
24};
25use miniscript::miniscript::limits::{MAX_PUBKEYS_IN_CHECKSIGADD, MAX_PUBKEYS_PER_MULTISIG};
26use miniscript::{
27    AbsLockTime, Legacy, Miniscript, RelLockTime, ScriptContext, Segwitv0, Tap, Terminal, Threshold,
28};
29use std::str::FromStr;
30use std::sync::Arc;
31
32/// BIP-341 NUMS H-point x-only coordinate. Used as the internal key when
33/// `Body::Tr { is_nums: true, .. }`.
34const NUMS_H_POINT_X_ONLY_HEX: &str =
35    "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0";
36
37/// Convert an md1 [`Descriptor`] AST to a
38/// `miniscript::Descriptor<DescriptorPublicKey>` for `chain` (the
39/// multipath alt selector). The trailing wildcard `/*` remains for
40/// `miniscript::Descriptor::at_derivation_index` to resolve.
41///
42/// `chain` is resolved in-place during key construction (multipath alt
43/// substituted into each `DescriptorXKey.derivation_path`); the resulting
44/// `Descriptor` is single-path.
45///
46/// # Errors
47///
48/// - [`Error::MissingPubkey`] / [`Error::InvalidXpubBytes`] /
49///   [`Error::MissingExplicitOrigin`] propagated from
50///   [`expand_per_at_n`].
51/// - [`Error::AddressDerivationFailed`] wrapping any miniscript-layer
52///   failure (type check, context error, unsupported fragment) or arity
53///   mismatch raised by the converter.
54pub fn to_miniscript_descriptor(
55    d: &Descriptor,
56    chain: u32,
57) -> Result<miniscript::Descriptor<DescriptorPublicKey>, Error> {
58    let expanded = expand_per_at_n(d)?;
59    let mut keys: Vec<DescriptorPublicKey> = Vec::with_capacity(expanded.len());
60    for e in &expanded {
61        // D1 (faithful per-key reconstruction): each `@N` derives at its
62        // OWN already-resolved use-site path (`e.use_site_path`), NOT the
63        // shared descriptor baseline. Passing `&d.use_site_path` here was
64        // the silent-wrong-address bug for per-cosigner override cards.
65        keys.push(build_descriptor_public_key(e, &e.use_site_path, chain)?);
66    }
67    node_to_descriptor(&d.tree, &keys)
68}
69
70/// Returns `true` if ANY use-site path on `d` requires a hardened public
71/// derivation step — the descriptor-level baseline (`d.use_site_path`) OR
72/// any per-`@N` entry in `d.tlv.use_site_path_overrides`. "Hardened
73/// anywhere" means a hardened wildcard (`/*h`) OR any hardened
74/// [`Alternative`](crate::use_site_path::Alternative) inside a multipath
75/// group.
76///
77/// BIP 32 forbids hardened derivation from an xpub, so an xpub-only restore
78/// cannot produce addresses for such a wallet. This is the single source of
79/// truth ("Point B") for the hardened-derivation refusal: the derivation
80/// boundary ([`Descriptor::derive_address`](crate::Descriptor::derive_address))
81/// uses it to refuse cleanly with [`Error::HardenedPublicDerivation`], and
82/// downstream consumers (e.g. the toolkit `restore` guard + advisory) reuse
83/// the SAME predicate so refusal and advisory stay in exact parity.
84///
85/// Note: this scans `use_site_path_overrides` directly (not the
86/// `expand_per_at_n` resolution), because the override set is exactly the
87/// per-`@N` divergent paths; a key with no override inherits the baseline,
88/// which is already covered by the `d.use_site_path` scan.
89pub fn has_hardened_use_site(d: &Descriptor) -> bool {
90    use_site_is_hardened(&d.use_site_path)
91        || d.tlv
92            .use_site_path_overrides
93            .as_deref()
94            .unwrap_or(&[])
95            .iter()
96            .any(|(_, usp)| use_site_is_hardened(usp))
97}
98
99/// Returns `true` if `u` has a hardened wildcard or any hardened multipath
100/// alternative.
101fn use_site_is_hardened(u: &UseSitePath) -> bool {
102    u.wildcard_hardened
103        || u.multipath
104            .as_deref()
105            .unwrap_or(&[])
106            .iter()
107            .any(|a| a.hardened)
108}
109
110/// Origin metadata for one expanded `@N`: `(fingerprint, origin_path)` when
111/// a `Fingerprints` TLV entry is present, else `None`. Shared by the
112/// single-path and multipath key builders so the two paths can never drift
113/// on origin/xkey assembly.
114type DescriptorOrigin = Option<(Fingerprint, DerivationPath)>;
115
116/// Assemble the `(origin, xkey)` pair common to both
117/// [`build_descriptor_public_key`] (single-path) and
118/// [`build_descriptor_multi_public_key`] (multipath) for one expanded `@N`.
119fn assemble_origin_and_xkey(
120    e: &ExpandedKey,
121) -> Result<(DescriptorOrigin, bitcoin::bip32::Xpub), Error> {
122    let xpub_bytes = e.xpub.ok_or(Error::MissingPubkey { idx: e.idx })?;
123    let xkey = xpub_from_tlv_bytes(e.idx, &xpub_bytes)?;
124    let origin = e.fingerprint.map(|fp| {
125        (
126            Fingerprint::from(fp),
127            origin_path_to_derivation(&e.origin_path),
128        )
129    });
130    Ok((origin, xkey))
131}
132
133/// `Wildcard::Hardened` for a `/*h` use-site, else `Wildcard::Unhardened`.
134/// Hardened wildcards are pre-refused at the derivation boundary
135/// ([`has_hardened_use_site`]); this only governs the rendered text.
136fn wildcard_for(use_site: &UseSitePath) -> Wildcard {
137    if use_site.wildcard_hardened {
138        Wildcard::Hardened
139    } else {
140        Wildcard::Unhardened
141    }
142}
143
144/// Build a `DescriptorPublicKey::XPub` for one expanded `@N`, with the
145/// chain alt substituted into `derivation_path` in-place. Single-path:
146/// resolves the `chain`-th multipath alternative now.
147fn build_descriptor_public_key(
148    e: &ExpandedKey,
149    use_site: &UseSitePath,
150    chain: u32,
151) -> Result<DescriptorPublicKey, Error> {
152    let (origin, xkey) = assemble_origin_and_xkey(e)?;
153
154    // Derivation path is the use-site multipath alt (without the trailing
155    // wildcard, which is handled via the `wildcard` field below).
156    let derivation_path = use_site_to_derivation_path(use_site, chain)?;
157
158    Ok(DescriptorPublicKey::XPub(DescriptorXKey {
159        origin,
160        xkey,
161        derivation_path,
162        wildcard: wildcard_for(use_site),
163    }))
164}
165
166/// Build a `DescriptorPublicKey` for one expanded `@N` carrying its FULL
167/// use-site multipath GROUP (not a single resolved chain). Used by
168/// [`to_miniscript_descriptor_multipath`] to render the faithful
169/// descriptor STRING with per-`@N` `<…;…>` groups.
170///
171/// - `e.use_site_path.multipath = Some(alts)` → `MultiXPub` with one
172///   `DerivationPath` per alternative (e.g. `<2;3>` → `[m/2, m/3]`).
173/// - `e.use_site_path.multipath = None` → a single-path `XPub` (bare `/*`).
174///
175/// rust-miniscript's `into_single_descriptors` selects each key's own alt
176/// at derivation time, so per-`@N` groups stay faithful end-to-end (and
177/// `sortedmulti` sorts the per-index-derived keys correctly).
178fn build_descriptor_multi_public_key(e: &ExpandedKey) -> Result<DescriptorPublicKey, Error> {
179    let (origin, xkey) = assemble_origin_and_xkey(e)?;
180    let use_site = &e.use_site_path;
181    let wildcard = wildcard_for(use_site);
182
183    match &use_site.multipath {
184        Some(alts) => {
185            let paths: Vec<DerivationPath> = alts
186                .iter()
187                .map(|a| {
188                    let child = if a.hardened {
189                        ChildNumber::from_hardened_idx(a.value)
190                            .unwrap_or(ChildNumber::Hardened { index: a.value })
191                    } else {
192                        ChildNumber::Normal { index: a.value }
193                    };
194                    DerivationPath::from(vec![child])
195                })
196                .collect();
197            let derivation_paths = DerivPaths::new(paths)
198                .ok_or_else(|| failed(format!("@{} multipath group is empty", e.idx)))?;
199            Ok(DescriptorPublicKey::MultiXPub(DescriptorMultiXKey {
200                origin,
201                xkey,
202                derivation_paths,
203                wildcard,
204            }))
205        }
206        None => Ok(DescriptorPublicKey::XPub(DescriptorXKey {
207            origin,
208            xkey,
209            derivation_path: DerivationPath::master(),
210            wildcard,
211        })),
212    }
213}
214
215/// Convert an md1 [`Descriptor`] AST to a *multipath*
216/// `miniscript::Descriptor<DescriptorPublicKey>` — one key per `@N`
217/// carrying its FULL resolved use-site multipath group (`<…;…>`), not the
218/// single-chain collapse of [`to_miniscript_descriptor`].
219///
220/// This is the faithful descriptor-STRING entry for per-cosigner use-site
221/// override cards: each `@N`'s group comes from `ExpandedKey.use_site_path`
222/// (per-`@N` override composed over the baseline) where `@N` == the
223/// `expand_per_at_n` Vec position — the unambiguous correspondence. A
224/// `None`-multipath override renders as a single-path `XPub` (bare `/*`)
225/// while sibling keys stay `MultiXPub` (the legal `Some`/`None` mix).
226///
227/// The trailing `/*` wildcards remain for
228/// `miniscript::Descriptor::into_single_descriptors` /
229/// `at_derivation_index` to resolve. The result is NOT single-path; callers
230/// that need addresses call `into_single_descriptors` (rust-miniscript
231/// selects each key's own alt per chain).
232///
233/// Hardened use-site cards are pre-refused by callers via
234/// [`has_hardened_use_site`]; a hardened alt reaching this builder renders a
235/// hardened child in the group (still a valid descriptor string, never a
236/// wrong address — it is never asked to *derive*).
237///
238/// # Errors
239///
240/// Same propagation as [`to_miniscript_descriptor`]:
241/// [`Error::MissingPubkey`] / [`Error::InvalidXpubBytes`] /
242/// [`Error::MissingExplicitOrigin`] from [`expand_per_at_n`], and
243/// [`Error::AddressDerivationFailed`] wrapping any miniscript-layer failure.
244pub fn to_miniscript_descriptor_multipath(
245    d: &Descriptor,
246) -> Result<miniscript::Descriptor<DescriptorPublicKey>, Error> {
247    let expanded = expand_per_at_n(d)?;
248    let mut keys: Vec<DescriptorPublicKey> = Vec::with_capacity(expanded.len());
249    for e in &expanded {
250        keys.push(build_descriptor_multi_public_key(e)?);
251    }
252    node_to_descriptor(&d.tree, &keys)
253}
254
255/// Translate an `OriginPath` into a `bip32::DerivationPath`.
256fn origin_path_to_derivation(p: &OriginPath) -> DerivationPath {
257    let children: Vec<ChildNumber> = p
258        .components
259        .iter()
260        .map(|c| {
261            if c.hardened {
262                ChildNumber::from_hardened_idx(c.value)
263                    .unwrap_or(ChildNumber::Hardened { index: c.value })
264            } else {
265                ChildNumber::from_normal_idx(c.value)
266                    .unwrap_or(ChildNumber::Normal { index: c.value })
267            }
268        })
269        .collect();
270    DerivationPath::from(children)
271}
272
273/// Build the per-key `derivation_path` from the use-site multipath: a
274/// single `ChildNumber` for `multipath[chain]`, or empty when no
275/// multipath. The trailing `/*` wildcard is encoded via the `wildcard`
276/// field, not the path.
277fn use_site_to_derivation_path(u: &UseSitePath, chain: u32) -> Result<DerivationPath, Error> {
278    let mut comps: Vec<ChildNumber> = Vec::new();
279    if let Some(alts) = &u.multipath {
280        let alt = alts
281            .get(chain as usize)
282            .ok_or(Error::ChainIndexOutOfRange {
283                chain,
284                alt_count: alts.len(),
285            })?;
286        if alt.hardened {
287            return Err(Error::HardenedPublicDerivation);
288        }
289        comps.push(ChildNumber::Normal { index: alt.value });
290    }
291    Ok(DerivationPath::from(comps))
292}
293
294/// Map an md1 top-level tree node onto a `miniscript::Descriptor`.
295fn node_to_descriptor(
296    node: &Node,
297    keys: &[DescriptorPublicKey],
298) -> Result<miniscript::Descriptor<DescriptorPublicKey>, Error> {
299    match (&node.tag, &node.body) {
300        (Tag::Pkh, Body::KeyArg { index }) => {
301            let pk = lookup_key(keys, *index)?;
302            miniscript::Descriptor::new_pkh(pk).map_err(|e| failed(e.to_string()))
303        }
304        (Tag::Wpkh, Body::KeyArg { index }) => {
305            let pk = lookup_key(keys, *index)?;
306            miniscript::Descriptor::new_wpkh(pk).map_err(|e| failed(e.to_string()))
307        }
308        (Tag::Sh, Body::Children(children)) if children.len() == 1 => {
309            sh_inner_to_descriptor(&children[0], keys)
310        }
311        (Tag::Wsh, Body::Children(children)) if children.len() == 1 => {
312            wsh_inner_to_descriptor(&children[0], keys)
313        }
314        (
315            Tag::Tr,
316            Body::Tr {
317                is_nums,
318                key_index,
319                tree,
320            },
321        ) => {
322            let internal_key = if *is_nums {
323                build_nums_internal_key()?
324            } else {
325                lookup_key(keys, *key_index)?
326            };
327            let script_tree = if let Some(t) = tree {
328                Some(tree_to_taptree(t, keys)?)
329            } else {
330                None
331            };
332            miniscript::Descriptor::new_tr(internal_key, script_tree)
333                .map_err(|e| failed(e.to_string()))
334        }
335        _ => Err(failed(format!(
336            "unsupported top-level tag {:?} with body shape",
337            node.tag
338        ))),
339    }
340}
341
342/// Build the NUMS-point `DescriptorPublicKey` (BIP-341 H-point as a
343/// single x-only descriptor key, no origin/path).
344fn build_nums_internal_key() -> Result<DescriptorPublicKey, Error> {
345    let x_only = bitcoin::secp256k1::XOnlyPublicKey::from_str(NUMS_H_POINT_X_ONLY_HEX)
346        .map_err(|e| failed(format!("NUMS x-only parse: {e}")))?;
347    Ok(DescriptorPublicKey::Single(SinglePub {
348        origin: None,
349        key: SinglePubKey::XOnly(x_only),
350    }))
351}
352
353/// Descriptor-level `wsh(...)` inner: choose between
354/// `Descriptor::new_wsh_sortedmulti` and `Descriptor::new_wsh(<Miniscript>)`.
355fn wsh_inner_to_descriptor(
356    inner: &Node,
357    keys: &[DescriptorPublicKey],
358) -> Result<miniscript::Descriptor<DescriptorPublicKey>, Error> {
359    if let (Tag::SortedMulti, Body::MultiKeys { k, indices }) = (&inner.tag, &inner.body) {
360        let thresh = build_multi_threshold::<{ MAX_PUBKEYS_PER_MULTISIG }>(
361            *k,
362            indices,
363            keys,
364            "wsh-sortedmulti",
365        )?;
366        return miniscript::Descriptor::new_wsh_sortedmulti(thresh)
367            .map_err(|e| failed(e.to_string()));
368    }
369    let ms = node_to_miniscript::<Segwitv0>(inner, keys)?;
370    miniscript::Descriptor::new_wsh(ms).map_err(|e| failed(e.to_string()))
371}
372
373/// Descriptor-level `sh(...)` inner: dispatch between `sh(wsh(...))`,
374/// `sh(wpkh(...))`, `sh(sortedmulti(...))`, and `sh(<miniscript>)`
375/// (Legacy context).
376fn sh_inner_to_descriptor(
377    inner: &Node,
378    keys: &[DescriptorPublicKey],
379) -> Result<miniscript::Descriptor<DescriptorPublicKey>, Error> {
380    match (&inner.tag, &inner.body) {
381        (Tag::Wsh, Body::Children(grand)) if grand.len() == 1 => {
382            let grandchild = &grand[0];
383            if let (Tag::SortedMulti, Body::MultiKeys { k, indices }) =
384                (&grandchild.tag, &grandchild.body)
385            {
386                let thresh = build_multi_threshold::<{ MAX_PUBKEYS_PER_MULTISIG }>(
387                    *k,
388                    indices,
389                    keys,
390                    "sh-wsh-sortedmulti",
391                )?;
392                return miniscript::Descriptor::new_sh_wsh_sortedmulti(thresh)
393                    .map_err(|e| failed(e.to_string()));
394            }
395            let ms = node_to_miniscript::<Segwitv0>(grandchild, keys)?;
396            miniscript::Descriptor::new_sh_wsh(ms).map_err(|e| failed(e.to_string()))
397        }
398        (Tag::Wpkh, Body::KeyArg { index }) => {
399            let pk = lookup_key(keys, *index)?;
400            miniscript::Descriptor::new_sh_wpkh(pk).map_err(|e| failed(e.to_string()))
401        }
402        (Tag::SortedMulti, Body::MultiKeys { k, indices }) => {
403            let thresh = build_multi_threshold::<{ MAX_PUBKEYS_PER_MULTISIG }>(
404                *k,
405                indices,
406                keys,
407                "sh-sortedmulti",
408            )?;
409            miniscript::Descriptor::new_sh_sortedmulti(thresh).map_err(|e| failed(e.to_string()))
410        }
411        _ => {
412            let ms = node_to_miniscript::<Legacy>(inner, keys)?;
413            miniscript::Descriptor::new_sh(ms).map_err(|e| failed(e.to_string()))
414        }
415    }
416}
417
418/// Recurse into a tap-script-tree node. Returns a `miniscript::TapTree`.
419fn tree_to_taptree(
420    node: &Node,
421    keys: &[DescriptorPublicKey],
422) -> Result<miniscript::descriptor::TapTree<DescriptorPublicKey>, Error> {
423    if let (Tag::TapTree, Body::Children(children)) = (&node.tag, &node.body) {
424        if children.len() != 2 {
425            return Err(failed(format!(
426                "Tag::TapTree expected 2 children, got {}",
427                children.len()
428            )));
429        }
430        let l = tree_to_taptree(&children[0], keys)?;
431        let r = tree_to_taptree(&children[1], keys)?;
432        return miniscript::descriptor::TapTree::combine(l, r)
433            .map_err(|e| failed(format!("TapTree depth: {e}")));
434    }
435    // Single bare leaf — including the v0.30 single-leaf wire optimization
436    // where `Body::Tr { tree: Some(<bare PkK Node>) }` skips the `Tag::TapTree`
437    // wrap.
438    let ms = node_to_miniscript::<Tap>(node, keys)?;
439    Ok(miniscript::descriptor::TapTree::leaf(Arc::new(ms)))
440}
441
442/// Convert a miniscript-leaf md1 node into a `Miniscript<Pk, Ctx>`.
443fn node_to_miniscript<Ctx>(
444    node: &Node,
445    keys: &[DescriptorPublicKey],
446) -> Result<Miniscript<DescriptorPublicKey, Ctx>, Error>
447where
448    Ctx: ScriptContext,
449{
450    let term: Terminal<DescriptorPublicKey, Ctx> = match (&node.tag, &node.body) {
451        (Tag::PkK, Body::KeyArg { index }) => {
452            // Phase E: bare PkK always emits as Check(pk_k(...)) since
453            // miniscript leaves require a `K` (check'd-key) at any
454            // satisfied position. md1 wire normalises by stripping the
455            // outer `c:` wrapper; re-apply here.
456            let pk = lookup_key(keys, *index)?;
457            let inner = Miniscript::from_ast(Terminal::PkK(pk)).map_err(into_failed)?;
458            Terminal::Check(Arc::new(inner))
459        }
460        (Tag::PkH, Body::KeyArg { index }) => {
461            let pk = lookup_key(keys, *index)?;
462            let inner = Miniscript::from_ast(Terminal::PkH(pk)).map_err(into_failed)?;
463            Terminal::Check(Arc::new(inner))
464        }
465        (Tag::Check, Body::Children(children)) => {
466            arity_eq(node.tag, children.len(), 1)?;
467            // Check-idempotence (`to-miniscript-check-pkh-double-wrap`): a
468            // `Tag::Check` over a BARE key tag denotes the same fragment as the
469            // bare tag — both mean `c:pk_k`/`c:pk_h` (type B), and the PkK/PkH
470            // arms above already re-apply `Check`. Wrapping a second `Check`
471            // yields `Check(Check(PkH))` = `c:` over type-B → "cannot wrap a
472            // fragment of type B". The toolkit walker (non-tap context) and
473            // pre-v0.30 md-cli cards both emit this `Tag::Check(Tag::PkK/PkH)`
474            // wire shape, and such cards are already engraved — the renderer
475            // must accept it. A `Tag::Check` whose child is NOT a bare key
476            // (`Check(Check(..))`, shape C `Check(or_i(pk_k,pk_k))`) still
477            // double-wraps below and correctly errors — never a wrong descriptor.
478            if matches!(
479                (&children[0].tag, &children[0].body),
480                (Tag::PkK | Tag::PkH, Body::KeyArg { .. })
481            ) {
482                return node_to_miniscript::<Ctx>(&children[0], keys);
483            }
484            Terminal::Check(Arc::new(node_to_miniscript::<Ctx>(&children[0], keys)?))
485        }
486        (Tag::Verify, Body::Children(children)) => {
487            arity_eq(node.tag, children.len(), 1)?;
488            Terminal::Verify(Arc::new(node_to_miniscript::<Ctx>(&children[0], keys)?))
489        }
490        (Tag::Swap, Body::Children(children)) => {
491            arity_eq(node.tag, children.len(), 1)?;
492            Terminal::Swap(Arc::new(node_to_miniscript::<Ctx>(&children[0], keys)?))
493        }
494        (Tag::Alt, Body::Children(children)) => {
495            arity_eq(node.tag, children.len(), 1)?;
496            Terminal::Alt(Arc::new(node_to_miniscript::<Ctx>(&children[0], keys)?))
497        }
498        (Tag::DupIf, Body::Children(children)) => {
499            arity_eq(node.tag, children.len(), 1)?;
500            Terminal::DupIf(Arc::new(node_to_miniscript::<Ctx>(&children[0], keys)?))
501        }
502        (Tag::NonZero, Body::Children(children)) => {
503            arity_eq(node.tag, children.len(), 1)?;
504            Terminal::NonZero(Arc::new(node_to_miniscript::<Ctx>(&children[0], keys)?))
505        }
506        (Tag::ZeroNotEqual, Body::Children(children)) => {
507            arity_eq(node.tag, children.len(), 1)?;
508            Terminal::ZeroNotEqual(Arc::new(node_to_miniscript::<Ctx>(&children[0], keys)?))
509        }
510        (Tag::AndV, Body::Children(children)) => {
511            arity_eq(node.tag, children.len(), 2)?;
512            let l = node_to_miniscript::<Ctx>(&children[0], keys)?;
513            let r = node_to_miniscript::<Ctx>(&children[1], keys)?;
514            Terminal::AndV(Arc::new(l), Arc::new(r))
515        }
516        (Tag::AndB, Body::Children(children)) => {
517            arity_eq(node.tag, children.len(), 2)?;
518            let l = node_to_miniscript::<Ctx>(&children[0], keys)?;
519            let r = node_to_miniscript::<Ctx>(&children[1], keys)?;
520            Terminal::AndB(Arc::new(l), Arc::new(r))
521        }
522        (Tag::AndOr, Body::Children(children)) => {
523            arity_eq(node.tag, children.len(), 3)?;
524            let a = node_to_miniscript::<Ctx>(&children[0], keys)?;
525            let b = node_to_miniscript::<Ctx>(&children[1], keys)?;
526            let c = node_to_miniscript::<Ctx>(&children[2], keys)?;
527            Terminal::AndOr(Arc::new(a), Arc::new(b), Arc::new(c))
528        }
529        (Tag::OrB, Body::Children(children)) => {
530            arity_eq(node.tag, children.len(), 2)?;
531            let l = node_to_miniscript::<Ctx>(&children[0], keys)?;
532            let r = node_to_miniscript::<Ctx>(&children[1], keys)?;
533            Terminal::OrB(Arc::new(l), Arc::new(r))
534        }
535        (Tag::OrC, Body::Children(children)) => {
536            arity_eq(node.tag, children.len(), 2)?;
537            let l = node_to_miniscript::<Ctx>(&children[0], keys)?;
538            let r = node_to_miniscript::<Ctx>(&children[1], keys)?;
539            Terminal::OrC(Arc::new(l), Arc::new(r))
540        }
541        (Tag::OrD, Body::Children(children)) => {
542            arity_eq(node.tag, children.len(), 2)?;
543            let l = node_to_miniscript::<Ctx>(&children[0], keys)?;
544            let r = node_to_miniscript::<Ctx>(&children[1], keys)?;
545            Terminal::OrD(Arc::new(l), Arc::new(r))
546        }
547        (Tag::OrI, Body::Children(children)) => {
548            arity_eq(node.tag, children.len(), 2)?;
549            let l = node_to_miniscript::<Ctx>(&children[0], keys)?;
550            let r = node_to_miniscript::<Ctx>(&children[1], keys)?;
551            Terminal::OrI(Arc::new(l), Arc::new(r))
552        }
553        (Tag::Thresh, Body::Variable { k, children }) => {
554            let mut subs: Vec<Arc<Miniscript<DescriptorPublicKey, Ctx>>> =
555                Vec::with_capacity(children.len());
556            for c in children {
557                subs.push(Arc::new(node_to_miniscript::<Ctx>(c, keys)?));
558            }
559            let thresh =
560                Threshold::<_, 0>::new(*k as usize, subs).map_err(|e| failed(e.to_string()))?;
561            Terminal::Thresh(thresh)
562        }
563        (Tag::Multi, Body::MultiKeys { k, indices }) => {
564            // `Terminal::Multi` is `Ctx`-generic at the variant level;
565            // rust-miniscript's `Miniscript::from_ast` enforces context-
566            // appropriateness (rejects Multi inside Tap, MultiA inside
567            // Segwitv0) via `check_global_consensus_validity`.
568            let thresh =
569                build_multi_threshold::<{ MAX_PUBKEYS_PER_MULTISIG }>(*k, indices, keys, "multi")?;
570            Terminal::Multi(thresh)
571        }
572        (Tag::MultiA, Body::MultiKeys { k, indices }) => {
573            let thresh = build_multi_threshold::<{ MAX_PUBKEYS_IN_CHECKSIGADD }>(
574                *k, indices, keys, "multi_a",
575            )?;
576            Terminal::MultiA(thresh)
577        }
578        (Tag::SortedMulti, Body::MultiKeys { .. }) => {
579            return Err(failed(
580                "Tag::SortedMulti must be the sole child of wsh/sh; cannot appear as a miniscript leaf"
581                    .to_string(),
582            ));
583        }
584        (Tag::SortedMultiA, Body::MultiKeys { .. }) => {
585            return Err(failed(
586                "Tag::SortedMultiA must be a tap-leaf root child; rust-miniscript v13 has no Terminal::SortedMultiA fragment"
587                    .to_string(),
588            ));
589        }
590        (Tag::After, Body::Timelock(v)) => {
591            let lt = AbsLockTime::from_consensus(*v).map_err(|e| failed(e.to_string()))?;
592            Terminal::After(lt)
593        }
594        (Tag::Older, Body::Timelock(v)) => {
595            let lt = RelLockTime::from_consensus(*v).map_err(|e| failed(e.to_string()))?;
596            Terminal::Older(lt)
597        }
598        (Tag::Sha256, Body::Hash256Body(h)) => {
599            let hash = sha256_from_bytes(h)?;
600            Terminal::Sha256(hash)
601        }
602        (Tag::Hash256, Body::Hash256Body(h)) => {
603            let hash = hash256_from_bytes(h)?;
604            Terminal::Hash256(hash)
605        }
606        (Tag::Ripemd160, Body::Hash160Body(h)) => {
607            let hash = ripemd160_from_bytes(h)?;
608            Terminal::Ripemd160(hash)
609        }
610        (Tag::Hash160, Body::Hash160Body(h)) => {
611            let hash = hash160_from_bytes(h)?;
612            Terminal::Hash160(hash)
613        }
614        (Tag::RawPkH, Body::Hash160Body(_)) => {
615            return Err(failed(
616                "Tag::RawPkH is not constructible through miniscript's public API".to_string(),
617            ));
618        }
619        (Tag::False, Body::Empty) => Terminal::False,
620        (Tag::True, Body::Empty) => Terminal::True,
621        (Tag::TapTree, _) => {
622            return Err(failed(
623                "Tag::TapTree is a tap-tree internal node, not a miniscript leaf".to_string(),
624            ));
625        }
626        (Tag::Tr, _) | (Tag::Wsh, _) | (Tag::Sh, _) | (Tag::Wpkh, _) | (Tag::Pkh, _) => {
627            return Err(failed(format!(
628                "top-level wrapper {:?} cannot appear inside a miniscript context",
629                node.tag
630            )));
631        }
632        _ => {
633            return Err(failed(format!(
634                "tag {:?} unsupported with body shape",
635                node.tag
636            )));
637        }
638    };
639    Miniscript::from_ast(term).map_err(into_failed)
640}
641
642fn lookup_key(keys: &[DescriptorPublicKey], idx: u8) -> Result<DescriptorPublicKey, Error> {
643    keys.get(idx as usize)
644        .cloned()
645        .ok_or_else(|| failed(format!("@{idx} out of range")))
646}
647
648fn build_multi_threshold<const MAX: usize>(
649    k: u8,
650    indices: &[u8],
651    keys: &[DescriptorPublicKey],
652    label: &str,
653) -> Result<Threshold<DescriptorPublicKey, MAX>, Error> {
654    let pks: Vec<DescriptorPublicKey> = indices
655        .iter()
656        .map(|i| lookup_key(keys, *i))
657        .collect::<Result<_, _>>()?;
658    Threshold::<DescriptorPublicKey, MAX>::new(k as usize, pks)
659        .map_err(|e| failed(format!("{label} threshold: {e}")))
660}
661
662fn arity_eq(tag: Tag, got: usize, expected: usize) -> Result<(), Error> {
663    if got != expected {
664        return Err(failed(format!(
665            "{tag:?} expected {expected} children, got {got}"
666        )));
667    }
668    Ok(())
669}
670
671fn failed(detail: String) -> Error {
672    Error::AddressDerivationFailed { detail }
673}
674
675fn into_failed(e: miniscript::Error) -> Error {
676    failed(e.to_string())
677}
678
679// ─── Pk::Sha256 / Pk::Hash256 / Pk::Ripemd160 / Pk::Hash160 construction ─
680
681fn sha256_from_bytes(h: &[u8; 32]) -> Result<bitcoin::hashes::sha256::Hash, Error> {
682    use bitcoin::hashes::Hash;
683    Ok(bitcoin::hashes::sha256::Hash::from_byte_array(*h))
684}
685
686fn hash256_from_bytes(h: &[u8; 32]) -> Result<miniscript::hash256::Hash, Error> {
687    use bitcoin::hashes::Hash;
688    Ok(miniscript::hash256::Hash::from_byte_array(*h))
689}
690
691fn ripemd160_from_bytes(h: &[u8; 20]) -> Result<bitcoin::hashes::ripemd160::Hash, Error> {
692    use bitcoin::hashes::Hash;
693    Ok(bitcoin::hashes::ripemd160::Hash::from_byte_array(*h))
694}
695
696fn hash160_from_bytes(h: &[u8; 20]) -> Result<bitcoin::hashes::hash160::Hash, Error> {
697    use bitcoin::hashes::Hash;
698    Ok(bitcoin::hashes::hash160::Hash::from_byte_array(*h))
699}