Skip to main content

md_codec/
canonical_origin.rs

1//! Canonical-origin map per spec §4 (v0.13 wallet-policy layer).
2//!
3//! Given the top-level wrapper of a descriptor template, return the canonical
4//! `path-from-master` for elided origin paths — or `None` if the wrapper shape
5//! is not in the canonical table (in which case the encoder must emit
6//! explicit `OriginPathOverrides` entries for all `@N` placeholders).
7//!
8//! Wrapper shape → canonical:
9//!
10//! | Shape                                  | Canonical             |
11//! |----------------------------------------|-----------------------|
12//! | `pkh(@N)` single-key                   | `m/44'/0'/0'`         |
13//! | `wpkh(@N)` single-key                  | `m/84'/0'/0'`         |
14//! | `tr(@N)` key-path only (no TapTree)    | `m/86'/0'/0'`         |
15//! | `wsh(multi/sortedmulti)`               | `m/48'/0'/0'/2'`      |
16//! | `sh(wsh(multi/sortedmulti))`           | `m/48'/0'/0'/1'`      |
17//! | `sh(sortedmulti)` legacy P2SH multi    | `None` (forced explicit) |
18//! | `tr(@N, TapTree)`                      | `None` (forced explicit) |
19//! | anything else                          | `None` (forced explicit) |
20
21use crate::origin_path::{OriginPath, PathComponent};
22use crate::tag::Tag;
23use crate::tree::{Body, Node};
24
25/// Build an [`OriginPath`] from a slice of `(hardened, value)` tuples.
26fn mk_origin(components: &[(bool, u32)]) -> OriginPath {
27    OriginPath {
28        components: components
29            .iter()
30            .map(|&(hardened, value)| PathComponent { hardened, value })
31            .collect(),
32    }
33}
34
35/// Returns `true` if `tag` is one of the multisig variants permitted directly
36/// inside a canonical `wsh(...)` or `sh(wsh(...))` wrapper (`multi` or
37/// `sortedmulti`).
38pub(crate) fn is_wsh_inner_multi(tag: Tag) -> bool {
39    matches!(tag, Tag::Multi | Tag::SortedMulti)
40}
41
42/// Compute the canonical origin path for the top-level wrapper `tree`, per
43/// spec §4. Returns `None` for shapes that require explicit
44/// `OriginPathOverrides` on the wire.
45pub fn canonical_origin(tree: &Node) -> Option<OriginPath> {
46    match (&tree.tag, &tree.body) {
47        // pkh(@N) single-key → m/44'/0'/0'
48        (Tag::Pkh, Body::KeyArg { .. }) => Some(mk_origin(&[(true, 44), (true, 0), (true, 0)])),
49        // wpkh(@N) single-key → m/84'/0'/0'
50        (Tag::Wpkh, Body::KeyArg { .. }) => Some(mk_origin(&[(true, 84), (true, 0), (true, 0)])),
51        // tr(@N) key-path only (no TapTree) → m/86'/0'/0'
52        (Tag::Tr, Body::Tr { tree: None, .. }) => {
53            Some(mk_origin(&[(true, 86), (true, 0), (true, 0)]))
54        }
55        // tr(@N, TapTree) → None (forced explicit)
56        (Tag::Tr, Body::Tr { tree: Some(_), .. }) => None,
57        // wsh(multi/sortedmulti) → m/48'/0'/0'/2'
58        (Tag::Wsh, Body::Children(children))
59            if children.len() == 1 && is_wsh_inner_multi(children[0].tag) =>
60        {
61            Some(mk_origin(&[(true, 48), (true, 0), (true, 0), (true, 2)]))
62        }
63        // sh(wsh(multi/sortedmulti)) → m/48'/0'/0'/1'
64        // sh(sortedmulti) legacy → None (handled by the catch-all below)
65        (Tag::Sh, Body::Children(children)) if children.len() == 1 => {
66            let inner = &children[0];
67            if inner.tag == Tag::Wsh {
68                if let Body::Children(grand) = &inner.body {
69                    if grand.len() == 1 && is_wsh_inner_multi(grand[0].tag) {
70                        return Some(mk_origin(&[(true, 48), (true, 0), (true, 0), (true, 1)]));
71                    }
72                }
73            }
74            None
75        }
76        // Everything else: bare wsh(@N), bare sh(@N), miniscript bodies, etc.
77        _ => None,
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::tree::{Body, Node};
85
86    fn pkh_at(n: u8) -> Node {
87        Node {
88            tag: Tag::Pkh,
89            body: Body::KeyArg { index: n },
90        }
91    }
92
93    fn wpkh_at(n: u8) -> Node {
94        Node {
95            tag: Tag::Wpkh,
96            body: Body::KeyArg { index: n },
97        }
98    }
99
100    fn tr_keypath(n: u8) -> Node {
101        Node {
102            tag: Tag::Tr,
103            body: Body::Tr {
104                is_nums: false,
105                key_index: n,
106                tree: None,
107            },
108        }
109    }
110
111    fn tr_with_taptree(n: u8) -> Node {
112        // Minimal non-empty TapTree: a single pk_k leaf wrapped in a TapTree
113        // node. The exact inner shape is not relevant — only that
114        // `tree: Some(_)` so the classifier sees a script-tree variant.
115        Node {
116            tag: Tag::Tr,
117            body: Body::Tr {
118                is_nums: false,
119                key_index: n,
120                tree: Some(Box::new(Node {
121                    tag: Tag::PkK,
122                    body: Body::KeyArg { index: 1 },
123                })),
124            },
125        }
126    }
127
128    fn multi_2of3() -> Node {
129        Node {
130            tag: Tag::Multi,
131            body: Body::MultiKeys {
132                k: 2,
133                indices: vec![0, 1, 2],
134            },
135        }
136    }
137
138    fn sortedmulti_2of3() -> Node {
139        Node {
140            tag: Tag::SortedMulti,
141            body: Body::MultiKeys {
142                k: 2,
143                indices: vec![0, 1, 2],
144            },
145        }
146    }
147
148    fn wsh_of(inner: Node) -> Node {
149        Node {
150            tag: Tag::Wsh,
151            body: Body::Children(vec![inner]),
152        }
153    }
154
155    fn sh_of(inner: Node) -> Node {
156        Node {
157            tag: Tag::Sh,
158            body: Body::Children(vec![inner]),
159        }
160    }
161
162    #[test]
163    fn pkh_at_n_returns_bip44_origin() {
164        let got = canonical_origin(&pkh_at(0)).unwrap();
165        assert_eq!(got, mk_origin(&[(true, 44), (true, 0), (true, 0)]));
166    }
167
168    #[test]
169    fn wpkh_at_n_returns_bip84_origin() {
170        let got = canonical_origin(&wpkh_at(0)).unwrap();
171        assert_eq!(got, mk_origin(&[(true, 84), (true, 0), (true, 0)]));
172    }
173
174    #[test]
175    fn tr_keypath_only_returns_bip86_origin() {
176        let got = canonical_origin(&tr_keypath(0)).unwrap();
177        assert_eq!(got, mk_origin(&[(true, 86), (true, 0), (true, 0)]));
178    }
179
180    #[test]
181    fn tr_with_taptree_returns_none() {
182        assert_eq!(canonical_origin(&tr_with_taptree(0)), None);
183    }
184
185    #[test]
186    fn wsh_multi_returns_bip48_type_2() {
187        let got = canonical_origin(&wsh_of(multi_2of3())).unwrap();
188        assert_eq!(
189            got,
190            mk_origin(&[(true, 48), (true, 0), (true, 0), (true, 2)])
191        );
192    }
193
194    #[test]
195    fn wsh_sortedmulti_returns_bip48_type_2() {
196        let got = canonical_origin(&wsh_of(sortedmulti_2of3())).unwrap();
197        assert_eq!(
198            got,
199            mk_origin(&[(true, 48), (true, 0), (true, 0), (true, 2)])
200        );
201    }
202
203    #[test]
204    fn sh_wsh_multi_returns_bip48_type_1() {
205        let got = canonical_origin(&sh_of(wsh_of(multi_2of3()))).unwrap();
206        assert_eq!(
207            got,
208            mk_origin(&[(true, 48), (true, 0), (true, 0), (true, 1)])
209        );
210    }
211
212    #[test]
213    fn sh_wsh_sortedmulti_returns_bip48_type_1() {
214        let got = canonical_origin(&sh_of(wsh_of(sortedmulti_2of3()))).unwrap();
215        assert_eq!(
216            got,
217            mk_origin(&[(true, 48), (true, 0), (true, 0), (true, 1)])
218        );
219    }
220
221    #[test]
222    fn sh_sortedmulti_legacy_returns_none() {
223        // sh(sortedmulti(...)) — legacy P2SH multi, not nested in wsh.
224        assert_eq!(canonical_origin(&sh_of(sortedmulti_2of3())), None);
225    }
226
227    #[test]
228    fn sh_multi_legacy_returns_none() {
229        // sh(multi(...)) — legacy P2SH multi, not nested in wsh.
230        assert_eq!(canonical_origin(&sh_of(multi_2of3())), None);
231    }
232
233    #[test]
234    fn bare_wsh_at_n_returns_none() {
235        // wsh(@N) — not allowed as a canonical shape; needs explicit override.
236        // The inner here is a single pk_k(@0) (single-key wsh, not multisig).
237        let inner = Node {
238            tag: Tag::PkK,
239            body: Body::KeyArg { index: 0 },
240        };
241        assert_eq!(canonical_origin(&wsh_of(inner)), None);
242    }
243
244    #[test]
245    fn bare_sh_at_n_returns_none() {
246        // sh(@N) — not allowed as a canonical shape.
247        let inner = Node {
248            tag: Tag::PkK,
249            body: Body::KeyArg { index: 0 },
250        };
251        assert_eq!(canonical_origin(&sh_of(inner)), None);
252    }
253
254    #[test]
255    fn wsh_with_miniscript_body_returns_none() {
256        // wsh(or_d(pk_k(@0), pk_h(@1))) — miniscript body, not a bare
257        // multi/sortedmulti. Must be forced explicit.
258        let inner = Node {
259            tag: Tag::OrD,
260            body: Body::Children(vec![
261                Node {
262                    tag: Tag::PkK,
263                    body: Body::KeyArg { index: 0 },
264                },
265                Node {
266                    tag: Tag::PkH,
267                    body: Body::KeyArg { index: 1 },
268                },
269            ]),
270        };
271        assert_eq!(canonical_origin(&wsh_of(inner)), None);
272    }
273
274    #[test]
275    fn tr_shape_disambiguation_pair_returns_different_verdicts() {
276        // Same outer Tag::Tr, but Body::Tr.tree differs: None → Some(BIP-86),
277        // Some(_) → None. Disambiguates on body shape, not just tag.
278        let keypath = tr_keypath(0);
279        let with_tree = tr_with_taptree(0);
280        assert!(canonical_origin(&keypath).is_some());
281        assert_eq!(canonical_origin(&with_tree), None);
282    }
283}