Skip to main content

md_codec/
render.rs

1//! Canonical `Descriptor` → BIP 388 `@N`-template renderer.
2//!
3//! Walks an md1 [`Descriptor`] AST (`Tag` / `Body` / `Node` / `UseSitePath`,
4//! no rust-miniscript) and emits the keyless wallet-policy template string with
5//! `@i` placeholders — e.g.
6//! `wsh(or_i(and_v(v:after(1000000),...),multi(3,@0/<0;1>/*,...)))`.
7//!
8//! This is the **single source of truth** for the template rendering: the
9//! `md` CLI (`md decode` / `md inspect`) delegates here, and the
10//! `mnemonic` toolkit's `inspect` renders the same `template:` line by calling
11//! [`descriptor_to_template`] — guaranteeing byte-identical output across both
12//! binaries.
13//!
14//! Lifted verbatim from `md-cli`'s `format/text.rs` (the renderer previously
15//! lived only in the CLI). The structural-guard error type changed from the
16//! CLI's `CliError::TemplateParse` to the dedicated [`RenderError`] so
17//! `md_codec::Error` stays a pure wire/decode taxonomy.
18
19use crate::encode::Descriptor;
20use crate::nums::NUMS_H_POINT_X_ONLY_HEX;
21use crate::tag::Tag;
22use crate::tree::{Body, Node};
23use crate::use_site_path::UseSitePath;
24use std::fmt::Write as _;
25
26/// Error returned by the [`descriptor_to_template`] renderer.
27///
28/// The renderer's `Err` arms are **fail-closed structural guards** that never
29/// fire on a decoder-produced [`Descriptor`] (a decoded AST is always
30/// well-formed). They exist so a foreign/test-fabricated tree with an
31/// impossible tag/body pairing produces a typed error instead of malformed
32/// output. Kept a *separate* type from [`crate::Error`] (the wire/decode
33/// taxonomy) because a text-render failure is not a wire error.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum RenderError {
36    /// A tree node had a structurally-malformed tag/body pairing the renderer
37    /// cannot serialize (e.g. a `Tag::Tr` without a `Body::Tr`).
38    MalformedTree(String),
39}
40
41impl std::fmt::Display for RenderError {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            RenderError::MalformedTree(m) => write!(f, "malformed descriptor tree: {m}"),
45        }
46    }
47}
48
49impl std::error::Error for RenderError {}
50
51/// Render a `Descriptor` back to a BIP 388 template string with `@i` placeholders.
52pub fn descriptor_to_template(d: &Descriptor) -> Result<String, RenderError> {
53    let mut out = String::new();
54    render_node(
55        &d.tree,
56        d.n,
57        &d.use_site_path,
58        d.tlv.use_site_path_overrides.as_deref(),
59        &mut out,
60    )?;
61    Ok(out)
62}
63
64fn render_node(
65    node: &Node,
66    n: u8,
67    default_usp: &UseSitePath,
68    overrides: Option<&[(u8, UseSitePath)]>,
69    out: &mut String,
70) -> Result<(), RenderError> {
71    match node.tag {
72        Tag::Wpkh => render_wrapper("wpkh", node, n, default_usp, overrides, out),
73        Tag::Pkh => render_wrapper("pkh", node, n, default_usp, overrides, out),
74        Tag::Wsh => render_wrapper("wsh", node, n, default_usp, overrides, out),
75        Tag::Sh => render_wrapper("sh", node, n, default_usp, overrides, out),
76        Tag::Tr => {
77            out.push_str("tr(");
78            match &node.body {
79                Body::Tr {
80                    is_nums,
81                    key_index,
82                    tree,
83                } => {
84                    // SPEC v0.30 §7: is_nums=true encodes the BIP-341 NUMS
85                    // H-point as the implicit internal key; render as the
86                    // literal x-only hex. Otherwise render @{key_index}.
87                    if *is_nums {
88                        out.push_str(NUMS_H_POINT_X_ONLY_HEX);
89                    } else {
90                        render_key(*key_index, default_usp, overrides, out)?;
91                    }
92                    if let Some(t) = tree {
93                        out.push(',');
94                        render_tap_node(t, n, default_usp, overrides, out)?;
95                    }
96                }
97                _ => {
98                    return Err(RenderError::MalformedTree(
99                        "Tag::Tr without Body::Tr".into(),
100                    ));
101                }
102            }
103            out.push(')');
104            Ok(())
105        }
106        Tag::Multi => render_multi("multi", node, default_usp, overrides, out),
107        Tag::SortedMulti => render_multi("sortedmulti", node, default_usp, overrides, out),
108        Tag::MultiA => render_multi("multi_a", node, default_usp, overrides, out),
109        Tag::SortedMultiA => render_multi("sortedmulti_a", node, default_usp, overrides, out),
110        Tag::PkK | Tag::PkH => match node.body {
111            Body::KeyArg { index } => {
112                // Tag::PkK on the wire encodes miniscript's `Terminal::PkK(K)`
113                // (BIP-379 sugar `pk(K)` = `c:pk_k(K)`, type B). Tag::PkH
114                // similarly encodes `Terminal::PkH(K)` (sugar `pkh(K)` =
115                // `c:pk_h(K)`, type B). Render as the sugar form so the
116                // emitted text re-parses through miniscript at the same
117                // type the encoder accepted; the bare `pk_k(K)` / `pk_h(K)`
118                // forms are type K, only valid as a `c:` child.
119                if matches!(node.tag, Tag::PkH) {
120                    out.push_str("pkh(");
121                } else {
122                    out.push_str("pk(");
123                }
124                render_key(index, default_usp, overrides, out)?;
125                out.push(')');
126                Ok(())
127            }
128            _ => Err(RenderError::MalformedTree(
129                "PkK/PkH without KeyArg body".into(),
130            )),
131        },
132        Tag::AndV => {
133            // and_v(left, right) — function-call syntax. Used inside tap-script
134            // leaves for and-conjunction / inheritance patterns.
135            let kids = match &node.body {
136                Body::Children(v) if v.len() == 2 => v,
137                _ => {
138                    return Err(RenderError::MalformedTree(
139                        "AndV body must be Children([2])".into(),
140                    ));
141                }
142            };
143            out.push_str("and_v(");
144            render_node(&kids[0], n, default_usp, overrides, out)?;
145            out.push(',');
146            render_node(&kids[1], n, default_usp, overrides, out)?;
147            out.push(')');
148            Ok(())
149        }
150        Tag::Verify => {
151            // `v:` wrapper — prefix syntax (no parens). The wrapped child is
152            // rendered inline; e.g. `v:pk(@1)`.
153            let inner = match &node.body {
154                Body::Children(v) if v.len() == 1 => &v[0],
155                _ => {
156                    return Err(RenderError::MalformedTree(
157                        "Verify body must be Children([1])".into(),
158                    ));
159                }
160            };
161            out.push_str("v:");
162            render_node(inner, n, default_usp, overrides, out)
163        }
164        Tag::Older => {
165            let v = match node.body {
166                Body::Timelock(v) => v,
167                _ => {
168                    return Err(RenderError::MalformedTree(
169                        "Older body must be Timelock".into(),
170                    ));
171                }
172            };
173            write!(out, "older({v})").unwrap();
174            Ok(())
175        }
176        Tag::After => {
177            let v = match node.body {
178                Body::Timelock(v) => v,
179                _ => {
180                    return Err(RenderError::MalformedTree(
181                        "After body must be Timelock".into(),
182                    ));
183                }
184            };
185            write!(out, "after({v})").unwrap();
186            Ok(())
187        }
188        Tag::AndB => render_binary("and_b", node, n, default_usp, overrides, out),
189        Tag::OrB => render_binary("or_b", node, n, default_usp, overrides, out),
190        Tag::OrC => render_binary("or_c", node, n, default_usp, overrides, out),
191        Tag::OrD => render_binary("or_d", node, n, default_usp, overrides, out),
192        Tag::OrI => render_binary("or_i", node, n, default_usp, overrides, out),
193        Tag::AndOr => {
194            // andor(a, b, c) — ternary "if a then b else c". Only ternary
195            // fragment in miniscript; Body::Children must have length 3.
196            let kids = match &node.body {
197                Body::Children(v) if v.len() == 3 => v,
198                _ => {
199                    return Err(RenderError::MalformedTree(
200                        "AndOr body must be Children([3])".into(),
201                    ));
202                }
203            };
204            out.push_str("andor(");
205            render_node(&kids[0], n, default_usp, overrides, out)?;
206            out.push(',');
207            render_node(&kids[1], n, default_usp, overrides, out)?;
208            out.push(',');
209            render_node(&kids[2], n, default_usp, overrides, out)?;
210            out.push(')');
211            Ok(())
212        }
213        Tag::Sha256 => render_hash256("sha256", &node.body, out),
214        Tag::Hash256 => render_hash256("hash256", &node.body, out),
215        Tag::Ripemd160 => render_hash160("ripemd160", &node.body, out),
216        Tag::Hash160 => render_hash160("hash160", &node.body, out),
217        Tag::Check | Tag::Swap | Tag::Alt | Tag::DupIf | Tag::NonZero | Tag::ZeroNotEqual => {
218            render_wrapper_chain(node, n, default_usp, overrides, out)
219        }
220        Tag::True => {
221            out.push('1');
222            Ok(())
223        }
224        Tag::False => {
225            out.push('0');
226            Ok(())
227        }
228        Tag::RawPkH => {
229            // Decode-side only — Tag::RawPkH carries a 20-byte hash in the
230            // wire format. Miniscript's RawPkH variant is constructible only
231            // from raw scripts (per upstream doc-comment), never from policy
232            // or descriptor APIs, so this arm exists for round-trip fidelity
233            // when md-codec encounters a RawPkH wire tag emitted by some
234            // other producer.
235            //
236            // Rendering choice: emit `expr_raw_pkh(<hex>)` (no underscore
237            // between `pk` and `h`; the parser-accepted checked form), not
238            // the bare-K Display form `expr_raw_pk_h(<hex>)` (with
239            // underscore). miniscript-rs's parser at `mod.rs:1017` only
240            // accepts `expr_raw_pkh`, which produces
241            // `Terminal::Check(Terminal::RawPkH(<hash>))` — type B. The bare
242            // `expr_raw_pk_h` Display form is type K (display.rs:248) and is
243            // an internal artifact, not a spec-level string. Emitting the
244            // checked form matches the v0.4.2 PkH pattern (bare `Tag::PkH`
245            // → `pkh(K)`, the type-B sugar; `Tag::RawPkH` → `expr_raw_pkh(<hex>)`,
246            // its type-B sugar) and produces output that re-parses through
247            // miniscript. Absorbs the would-be `Check(RawPkH)` shorthand-
248            // collapse case at the bare arm, so `render_wrapper_chain`
249            // needs no parallel extension.
250            let h = match &node.body {
251                Body::Hash160Body(h) => h,
252                _ => {
253                    return Err(RenderError::MalformedTree(
254                        "RawPkH body must be Hash160Body".into(),
255                    ));
256                }
257            };
258            out.push_str("expr_raw_pkh(");
259            for byte in h {
260                write!(out, "{byte:02x}").unwrap();
261            }
262            out.push(')');
263            Ok(())
264        }
265        Tag::Thresh => {
266            // thresh(k, c1, c2, ..., cn) — k-of-n threshold over arbitrary
267            // miniscript fragments (distinct from Multi/MultiA which take only
268            // keys). Each child is rendered recursively.
269            let (k, children) = match &node.body {
270                Body::Variable { k, children } => (*k, children),
271                _ => {
272                    return Err(RenderError::MalformedTree(
273                        "Thresh body must be Variable".into(),
274                    ));
275                }
276            };
277            write!(out, "thresh({k}").unwrap();
278            for child in children {
279                out.push(',');
280                render_node(child, n, default_usp, overrides, out)?;
281            }
282            out.push(')');
283            Ok(())
284        }
285        other => Err(RenderError::MalformedTree(format!(
286            "unsupported tag in render: {other:?}"
287        ))),
288    }
289}
290
291/// Render a 32-byte-hash literal (sha256, hash256). Body must be Hash256Body.
292fn render_hash256(name: &str, body: &Body, out: &mut String) -> Result<(), RenderError> {
293    let h = match body {
294        Body::Hash256Body(h) => h,
295        _ => {
296            return Err(RenderError::MalformedTree(format!(
297                "{name} body must be Hash256Body"
298            )));
299        }
300    };
301    out.push_str(name);
302    out.push('(');
303    for byte in h {
304        write!(out, "{byte:02x}").unwrap();
305    }
306    out.push(')');
307    Ok(())
308}
309
310/// Render a 20-byte-hash literal (ripemd160, hash160). Body must be Hash160Body.
311fn render_hash160(name: &str, body: &Body, out: &mut String) -> Result<(), RenderError> {
312    let h = match body {
313        Body::Hash160Body(h) => h,
314        _ => {
315            return Err(RenderError::MalformedTree(format!(
316                "{name} body must be Hash160Body"
317            )));
318        }
319    };
320    out.push_str(name);
321    out.push('(');
322    for byte in h {
323        write!(out, "{byte:02x}").unwrap();
324    }
325    out.push(')');
326    Ok(())
327}
328
329/// Render a chain of single-letter prefix wrappers as miniscript's canonical
330/// concatenated form: e.g. `Swap(NonZero(DupIf(X)))` → `snj:X` (not
331/// `s:n:j:X`). Walks down the wrapper spine, accumulating letters, then
332/// renders the innermost non-wrapper fragment after a single `:`.
333///
334/// # Caller contract
335///
336/// Reachable from exactly one site: the wrapper-chain dispatch arm in
337/// [`render_node`] for tags `Check | Swap | Alt | DupIf | NonZero |
338/// ZeroNotEqual`. The function MUST NOT be called with any other tag —
339/// its first-iteration loop body relies on the head being a wrapper, and
340/// passing a non-wrapper would emit a malformed bare `:` followed by the
341/// inner render. The `debug_assert!` below pins this invariant in tests
342/// and debug builds. A structural restructure that peels the first letter
343/// unconditionally would also work but adds complexity for no live-bug
344/// benefit; the assert is sufficient.
345///
346/// Special cases:
347/// - `Check(PkK)` / `Check(PkH)` collapse to `pk(K)` / `pkh(K)`. v0.30 SPEC
348///   §5.1 (Q12 — walker normalization) makes the v0.30 md-cli walker emit
349///   bare `Tag::PkK` / `Tag::PkH` at every key-leaf position, so this arm
350///   is unreachable on v0.30-produced wires (the bare-PkK/PkH arm in
351///   [`render_node`] handles the shorthand directly). Retained as defensive
352///   coverage for foreign/legacy/test-fabricated wires that still carry the
353///   wrapped shape.
354/// - When `n:` (Tag::ZeroNotEqual) appears immediately before a `0` literal,
355///   miniscript prints `n0` not `n:0`. Phase 4b doesn't pin this corner; bare
356///   `0` at top-level is structurally degenerate. Handle if a future test
357///   surfaces it.
358fn render_wrapper_chain(
359    node: &Node,
360    n: u8,
361    default_usp: &UseSitePath,
362    overrides: Option<&[(u8, UseSitePath)]>,
363    out: &mut String,
364) -> Result<(), RenderError> {
365    // The single dispatch arm at render_node guarantees `node.tag` is one of
366    // the six wrapper tags (Check/Swap/Alt/DupIf/NonZero/ZeroNotEqual), so the
367    // first iteration of the loop below always assigns a non-None letter.
368    // Guard the empty-prefix case in debug builds to make the invariant
369    // explicit (release builds skip the assertion; the invariant is upheld by
370    // the dispatch site, not by render_wrapper_chain itself).
371    debug_assert!(
372        matches!(
373            node.tag,
374            Tag::Check | Tag::Swap | Tag::Alt | Tag::DupIf | Tag::NonZero | Tag::ZeroNotEqual
375        ),
376        "render_wrapper_chain called on non-wrapper tag {:?}",
377        node.tag
378    );
379    let mut prefix = String::new();
380    let mut current = node;
381    loop {
382        let letter = match current.tag {
383            Tag::Check => Some('c'),
384            Tag::Swap => Some('s'),
385            Tag::Alt => Some('a'),
386            Tag::DupIf => Some('d'),
387            Tag::NonZero => Some('j'),
388            Tag::ZeroNotEqual => Some('n'),
389            _ => None,
390        };
391        match letter {
392            Some(c) => {
393                prefix.push(c);
394                current = match &current.body {
395                    Body::Children(v) if v.len() == 1 => &v[0],
396                    _ => {
397                        return Err(RenderError::MalformedTree(format!(
398                            "{c}: wrapper body must be Children([1])"
399                        )));
400                    }
401                };
402            }
403            None => break,
404        }
405    }
406    // After collapsing the chain, if the deepest inner is PkK or PkH and the
407    // chain ends in `c`, emit the miniscript shorthand `pk(K)` / `pkh(K)`.
408    // (See the bare-PkK/PkH arm above for the BIP-379-sugar-form rationale.)
409    if prefix.ends_with('c') && matches!(current.tag, Tag::PkK | Tag::PkH) {
410        let prefix_no_c = &prefix[..prefix.len() - 1];
411        if !prefix_no_c.is_empty() {
412            out.push_str(prefix_no_c);
413            out.push(':');
414        }
415        let idx = match current.body {
416            Body::KeyArg { index } => index,
417            _ => {
418                return Err(RenderError::MalformedTree(
419                    "Check(PkK/PkH) inner body must be KeyArg".into(),
420                ));
421            }
422        };
423        if matches!(current.tag, Tag::PkH) {
424            out.push_str("pkh(");
425        } else {
426            out.push_str("pk(");
427        }
428        render_key(idx, default_usp, overrides, out)?;
429        out.push(')');
430        return Ok(());
431    }
432    out.push_str(&prefix);
433    out.push(':');
434    render_node(current, n, default_usp, overrides, out)
435}
436
437/// Render a binary fragment `name(left, right)` — used for and_b, or_b, or_c,
438/// or_d, or_i. Body::Children must have exactly 2 elements.
439fn render_binary(
440    name: &str,
441    node: &Node,
442    n: u8,
443    default_usp: &UseSitePath,
444    overrides: Option<&[(u8, UseSitePath)]>,
445    out: &mut String,
446) -> Result<(), RenderError> {
447    let kids = match &node.body {
448        Body::Children(v) if v.len() == 2 => v,
449        _ => {
450            return Err(RenderError::MalformedTree(format!(
451                "{name} body must be Children([2])"
452            )));
453        }
454    };
455    out.push_str(name);
456    out.push('(');
457    render_node(&kids[0], n, default_usp, overrides, out)?;
458    out.push(',');
459    render_node(&kids[1], n, default_usp, overrides, out)?;
460    out.push(')');
461    Ok(())
462}
463
464/// Render a single-arity wrapper (wsh, sh, wpkh, pkh) — both `Children([inner])`
465/// and `KeyArg{index}` (Wpkh/Pkh leaf form) work.
466fn render_wrapper(
467    name: &str,
468    node: &Node,
469    n: u8,
470    default_usp: &UseSitePath,
471    overrides: Option<&[(u8, UseSitePath)]>,
472    out: &mut String,
473) -> Result<(), RenderError> {
474    out.push_str(name);
475    out.push('(');
476    match &node.body {
477        Body::KeyArg { index } => render_key(*index, default_usp, overrides, out)?,
478        Body::Children(v) if v.len() == 1 => render_node(&v[0], n, default_usp, overrides, out)?,
479        _ => {
480            return Err(RenderError::MalformedTree(format!(
481                "{name} body must be KeyArg or Children([1])"
482            )));
483        }
484    }
485    out.push(')');
486    Ok(())
487}
488
489fn render_multi(
490    name: &str,
491    node: &Node,
492    default_usp: &UseSitePath,
493    overrides: Option<&[(u8, UseSitePath)]>,
494    out: &mut String,
495) -> Result<(), RenderError> {
496    // v0.30 Phase C: multi-family bodies carry raw key indices, not child Nodes.
497    let (k, indices) = match &node.body {
498        Body::MultiKeys { k, indices } => (*k, indices),
499        _ => {
500            return Err(RenderError::MalformedTree(format!(
501                "{name} body must be MultiKeys"
502            )));
503        }
504    };
505    write!(out, "{name}({k}").unwrap();
506    for idx in indices {
507        out.push(',');
508        render_key(*idx, default_usp, overrides, out)?;
509    }
510    out.push(')');
511    Ok(())
512}
513
514/// Render a tap-tree node. Branches → `{left,right}`; leaves → render their body
515/// directly (no wrapper around the leaf).
516fn render_tap_node(
517    node: &Node,
518    n: u8,
519    default_usp: &UseSitePath,
520    overrides: Option<&[(u8, UseSitePath)]>,
521    out: &mut String,
522) -> Result<(), RenderError> {
523    if matches!(node.tag, Tag::TapTree) {
524        let children = match &node.body {
525            Body::Children(v) if v.len() == 2 => v,
526            _ => {
527                return Err(RenderError::MalformedTree(
528                    "TapTree must have Children([2])".into(),
529                ));
530            }
531        };
532        out.push('{');
533        render_tap_node(&children[0], n, default_usp, overrides, out)?;
534        out.push(',');
535        render_tap_node(&children[1], n, default_usp, overrides, out)?;
536        out.push('}');
537        Ok(())
538    } else {
539        render_node(node, n, default_usp, overrides, out)
540    }
541}
542
543fn render_key(
544    idx: u8,
545    default_usp: &UseSitePath,
546    overrides: Option<&[(u8, UseSitePath)]>,
547    out: &mut String,
548) -> Result<(), RenderError> {
549    let usp = overrides
550        .and_then(|v| v.iter().find(|(i, _)| *i == idx).map(|(_, u)| u))
551        .unwrap_or(default_usp);
552    write!(out, "@{idx}").unwrap();
553    if let Some(alts) = &usp.multipath {
554        out.push_str("/<");
555        for (n, alt) in alts.iter().enumerate() {
556            if n > 0 {
557                out.push(';');
558            }
559            write!(out, "{}", alt.value).unwrap();
560            if alt.hardened {
561                out.push('\'');
562            }
563        }
564        out.push_str(">/*");
565    } else {
566        out.push_str("/*");
567    }
568    if usp.wildcard_hardened {
569        out.push('\'');
570    }
571    Ok(())
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577
578    /// v0.4.3 — bare `Tag::RawPkH` rendering, unit-pinned at the `render_node`
579    /// level. Since v0.10.0 the walker DOES emit `Tag::RawPkH` (for the parseable
580    /// `wsh(c:expr_raw_pkh(<hash>))` form → `Wsh → Check → RawPkH`); the walk half
581    /// is pinned by `walk_rawpkh_wsh_check_emits_rawpkh_node` in `md-cli`'s
582    /// `parse::template`. This test still constructs the Node directly to pin the
583    /// bare-node rendering invariant in isolation (no `@N` placeholder is
584    /// involved, so the full `parse_template` pipeline — which requires
585    /// placeholders — is not the entry point here). Asserts the output matches
586    /// the parser-accepted Display form `expr_raw_pkh(<hex>)` — see the
587    /// doc-comment on the arm at `render_node`'s Tag::RawPkH match for the
588    /// rationale.
589    ///
590    /// Relocated from `md-cli` `format/text.rs` when the renderer moved into
591    /// md-codec (the md-cli copy called the now-removed local `render_node`).
592    #[test]
593    fn render_bare_rawpkh_emits_expr_raw_pkh() {
594        let node = Node {
595            tag: Tag::RawPkH,
596            body: Body::Hash160Body([0u8; 20]),
597        };
598        let usp = UseSitePath::standard_multipath();
599        let mut out = String::new();
600        render_node(
601            &node, /* n */ 1, &usp, /* overrides */ None, &mut out,
602        )
603        .expect("render_node Tag::RawPkH must succeed");
604        assert_eq!(
605            out,
606            "expr_raw_pkh(0000000000000000000000000000000000000000)",
607        );
608    }
609}