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