md_codec/derive.rs
1//! Address derivation (v0.32).
2//!
3//! v0.32 replaces the v0.14-era hand-rolled 5-shape allow-list with an
4//! AST → [`miniscript::Descriptor`] converter
5//! ([`crate::to_miniscript::to_miniscript_descriptor`]) and delegates
6//! address rendering to rust-miniscript. Any BIP-388-parseable shape
7//! derives — multi-leaf tap-trees, `tr(NUMS, ...)`, `sh(multi)`, arbitrary
8//! `wsh(<miniscript>)`, and any tap-leaf miniscript fragment included.
9//!
10//! Feature-gated: requires `derive` (default-on). Pure-codec consumers can
11//! opt out via `default-features = false`.
12//!
13//! ### What this module does NOT do
14//!
15//! - Origin path is not consulted. Origin is the path *to* the xpub from
16//! the master seed; address derivation starts at the xpub. The recorded
17//! origin matters for signing flows (PSBT key-source metadata), not for
18//! getting an address.
19//! - Master fingerprint (`Fingerprints` TLV) is unused for the same
20//! reason — it identifies the master, not the derivation root.
21//! - Hardened use-site components are rejected. Hardened public derivation
22//! is forbidden by BIP 32; an xpub-only restore cannot produce addresses
23//! for a wallet whose use-site path has a hardened alternative or
24//! hardened wildcard.
25
26#[cfg(feature = "derive")]
27use crate::encode::Descriptor;
28#[cfg(feature = "derive")]
29use crate::error::Error;
30#[cfg(feature = "derive")]
31use bitcoin::NetworkKind;
32#[cfg(feature = "derive")]
33use bitcoin::address::NetworkUnchecked;
34#[cfg(feature = "derive")]
35use bitcoin::bip32::{ChainCode, ChildNumber, Fingerprint, Xpub};
36#[cfg(feature = "derive")]
37use bitcoin::secp256k1::PublicKey;
38#[cfg(feature = "derive")]
39use bitcoin::{Address, Network};
40
41/// Reconstruct an [`Xpub`] from a 65-byte `Pubkeys` TLV payload.
42///
43/// Layout: `bytes[0..32]` = chain code; `bytes[32..65]` = compressed
44/// public key. The four BIP 32 metadata fields (`network`, `depth`,
45/// `parent_fingerprint`, `child_number`) are not used by
46/// [`Xpub::derive_pub`] (only `chain_code` and `public_key` participate
47/// in `CKDpub`); they are filled with safe placeholders.
48#[cfg(feature = "derive")]
49pub(crate) fn xpub_from_tlv_bytes(idx: u8, bytes: &[u8; 65]) -> Result<Xpub, Error> {
50 let chain_code_bytes: [u8; 32] = bytes[0..32]
51 .try_into()
52 .expect("32-byte slice is statically sized");
53 let chain_code = ChainCode::from(chain_code_bytes);
54 let public_key =
55 PublicKey::from_slice(&bytes[32..65]).map_err(|_| Error::InvalidXpubBytes { idx })?;
56 Ok(Xpub {
57 network: NetworkKind::Main,
58 depth: 0,
59 parent_fingerprint: Fingerprint::default(),
60 child_number: ChildNumber::Normal { index: 0 },
61 public_key,
62 chain_code,
63 })
64}
65
66#[cfg(feature = "derive")]
67impl Descriptor {
68 /// Derive the address at `(chain, index)` for this descriptor on
69 /// `network`.
70 ///
71 /// `chain` selects the use-site multipath alternative (e.g. `0` =
72 /// receive, `1` = change for the standard `<0;1>/*` form). `index` is
73 /// the trailing wildcard child number.
74 ///
75 /// Returns an [`Address<NetworkUnchecked>`]; callers can
76 /// `.assume_checked()` (when they trust the network parameter) or
77 /// `.require_network(network)` to lock it down.
78 ///
79 /// # Errors
80 ///
81 /// - [`Error::MissingPubkey`] when any `@N` lacks an xpub.
82 /// - [`Error::InvalidXpubBytes`] when an xpub's 33-byte pubkey field
83 /// doesn't parse as a valid secp256k1 point.
84 /// - [`Error::ChainIndexOutOfRange`] when `chain` is out of range for
85 /// the use-site multipath.
86 /// - [`Error::HardenedPublicDerivation`] when the use-site path
87 /// requires a hardened derivation step.
88 /// - [`Error::MissingExplicitOrigin`] propagated from
89 /// [`crate::canonicalize::expand_per_at_n`].
90 /// - [`Error::AddressDerivationFailed`] for any miniscript-layer
91 /// failure (type check, context error, unsupported fragment).
92 pub fn derive_address(
93 &self,
94 chain: u32,
95 index: u32,
96 network: Network,
97 ) -> Result<Address<NetworkUnchecked>, Error> {
98 // Pre-flight: hardened public-key derivation rejection (BIP-32
99 // forbids). The shared `has_hardened_use_site` predicate (Point B)
100 // covers BOTH the baseline use-site path AND every per-`@N`
101 // override — a hardened wildcard OR any hardened multipath
102 // alternative, anywhere. The pre-fix baseline-only checks missed a
103 // hardened alternative inside an override, which then surfaced only
104 // as a generic `AddressDerivationFailed` deep in the converter.
105 if crate::to_miniscript::has_hardened_use_site(self) {
106 return Err(Error::HardenedPublicDerivation);
107 }
108 // Pre-flight: chain index in range (resolved against the baseline
109 // use-site multipath alt-count, which bounds the supported chains).
110 if let Some(alts) = &self.use_site_path.multipath {
111 if (chain as usize) >= alts.len() {
112 return Err(Error::ChainIndexOutOfRange {
113 chain,
114 alt_count: alts.len(),
115 });
116 }
117 } else if chain != 0 {
118 return Err(Error::ChainIndexOutOfRange {
119 chain,
120 alt_count: 0,
121 });
122 }
123
124 let desc = crate::to_miniscript::to_miniscript_descriptor(self, chain)?;
125 let definite =
126 desc.at_derivation_index(index)
127 .map_err(|e| Error::AddressDerivationFailed {
128 detail: e.to_string(),
129 })?;
130 let addr = definite
131 .address(network)
132 .map_err(|e| Error::AddressDerivationFailed {
133 detail: e.to_string(),
134 })?;
135 Ok(addr.into_unchecked())
136 }
137}
138
139#[cfg(all(test, feature = "derive"))]
140mod tests {
141 use super::*;
142 use crate::origin_path::{OriginPath, PathComponent, PathDecl, PathDeclPaths};
143 use crate::tag::Tag;
144 use crate::tlv::TlvSection;
145 use crate::tree::{Body, Node};
146 use crate::use_site_path::{Alternative, UseSitePath};
147
148 // ─── xpub_from_tlv_bytes ─────────────────────────────────────────
149
150 #[test]
151 fn xpub_from_tlv_bytes_rejects_invalid_pubkey() {
152 // 33 zero bytes is not a valid compressed pubkey.
153 let bytes = [0u8; 65];
154 assert!(matches!(
155 xpub_from_tlv_bytes(7, &bytes),
156 Err(Error::InvalidXpubBytes { idx: 7 })
157 ));
158 }
159
160 fn bip84_origin() -> OriginPath {
161 OriginPath {
162 components: vec![
163 PathComponent {
164 hardened: true,
165 value: 84,
166 },
167 PathComponent {
168 hardened: true,
169 value: 0,
170 },
171 PathComponent {
172 hardened: true,
173 value: 0,
174 },
175 ],
176 }
177 }
178
179 fn one_test_xpub_bytes() -> [u8; 65] {
180 let mut bytes = [0u8; 65];
181 bytes[0..32].copy_from_slice(&[0x42; 32]);
182 bytes[32] = 0x02;
183 bytes[33..].copy_from_slice(&[
184 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, 0x87,
185 0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, 0x5B,
186 0x16, 0xF8, 0x17, 0x98,
187 ]);
188 bytes
189 }
190
191 #[test]
192 fn derive_address_missing_pubkey_for_partial_keys() {
193 // 2-of-2 wsh-sortedmulti with only @0 populated.
194 let d = Descriptor {
195 n: 2,
196 path_decl: PathDecl {
197 n: 2,
198 paths: PathDeclPaths::Shared(OriginPath {
199 components: vec![
200 PathComponent {
201 hardened: true,
202 value: 48,
203 },
204 PathComponent {
205 hardened: true,
206 value: 0,
207 },
208 PathComponent {
209 hardened: true,
210 value: 0,
211 },
212 PathComponent {
213 hardened: true,
214 value: 2,
215 },
216 ],
217 }),
218 },
219 use_site_path: UseSitePath::standard_multipath(),
220 tree: Node {
221 tag: Tag::Wsh,
222 body: Body::Children(vec![Node {
223 tag: Tag::SortedMulti,
224 body: Body::MultiKeys {
225 k: 2,
226 indices: vec![0, 1],
227 },
228 }]),
229 },
230 tlv: {
231 let mut t = TlvSection::new_empty();
232 t.pubkeys = Some(vec![(0u8, one_test_xpub_bytes())]);
233 t
234 },
235 };
236 let err = d.derive_address(0, 0, Network::Bitcoin).unwrap_err();
237 assert!(matches!(err, Error::MissingPubkey { idx: 1 }));
238 }
239
240 #[test]
241 fn derive_address_chain_out_of_range() {
242 let d = Descriptor {
243 n: 1,
244 path_decl: PathDecl {
245 n: 1,
246 paths: PathDeclPaths::Shared(bip84_origin()),
247 },
248 use_site_path: UseSitePath::standard_multipath(), // alt-count=2
249 tree: Node {
250 tag: Tag::Wpkh,
251 body: Body::KeyArg { index: 0 },
252 },
253 tlv: {
254 let mut t = TlvSection::new_empty();
255 t.pubkeys = Some(vec![(0u8, one_test_xpub_bytes())]);
256 t
257 },
258 };
259 let err = d.derive_address(5, 0, Network::Bitcoin).unwrap_err();
260 assert!(matches!(
261 err,
262 Error::ChainIndexOutOfRange {
263 chain: 5,
264 alt_count: 2
265 }
266 ));
267 }
268
269 #[test]
270 fn derive_address_hardened_wildcard_rejected() {
271 let d = Descriptor {
272 n: 1,
273 path_decl: PathDecl {
274 n: 1,
275 paths: PathDeclPaths::Shared(bip84_origin()),
276 },
277 use_site_path: UseSitePath {
278 multipath: Some(vec![
279 Alternative {
280 hardened: false,
281 value: 0,
282 },
283 Alternative {
284 hardened: false,
285 value: 1,
286 },
287 ]),
288 wildcard_hardened: true,
289 },
290 tree: Node {
291 tag: Tag::Wpkh,
292 body: Body::KeyArg { index: 0 },
293 },
294 tlv: {
295 let mut t = TlvSection::new_empty();
296 t.pubkeys = Some(vec![(0u8, one_test_xpub_bytes())]);
297 t
298 },
299 };
300 let err = d.derive_address(0, 0, Network::Bitcoin).unwrap_err();
301 assert!(matches!(err, Error::HardenedPublicDerivation));
302 }
303}