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