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 ¤t.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}