zcash_keys/keys/
zcashd.rs1use alloc::string::{String, ToString};
4use bip0039::{English, Mnemonic};
5use regex::Regex;
6use secrecy::{ExposeSecret, SecretVec};
7use zcash_protocol::consensus::NetworkConstants;
8use zip32::{AccountId, ChildIndex};
9
10pub fn derive_mnemonic(legacy_seed: &SecretVec<u8>) -> Option<Mnemonic> {
16 if legacy_seed.expose_secret().len() != 32 {
17 return None;
18 }
19
20 let mut offset = 0u8;
21 loop {
22 let mut entropy = legacy_seed.expose_secret().clone();
23 entropy[0] += offset;
24 match Mnemonic::<English>::from_entropy(entropy) {
25 Ok(m) => {
26 return Some(m);
27 }
28 Err(_) => {
29 if offset == 0xFF {
30 return None;
31 } else {
32 offset += 1;
33 }
34 }
35 }
36 }
37}
38
39#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub struct LegacyAddressIndex(u32);
45
46impl TryFrom<u32> for LegacyAddressIndex {
47 type Error = ();
48
49 fn try_from(id: u32) -> Result<Self, Self::Error> {
50 if id < (1 << 31) {
53 Ok(Self(id))
54 } else {
55 Err(())
56 }
57 }
58}
59
60impl From<LegacyAddressIndex> for u32 {
61 fn from(id: LegacyAddressIndex) -> Self {
62 id.0
63 }
64}
65
66impl From<LegacyAddressIndex> for ChildIndex {
67 fn from(id: LegacyAddressIndex) -> Self {
68 ChildIndex::hardened(id.0)
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
75pub enum ZcashdHdDerivation {
76 Zip32 { account_id: AccountId },
78 Post470LegacySapling { address_index: LegacyAddressIndex },
82}
83
84#[derive(Debug)]
86pub enum PathParseError {
87 PathInvalid,
90 CoinTypeInvalid(String),
92 CoinTypeMismatch { expected: u32, actual: u32 },
95 AccountIndexInvalid(String),
98 AccountIdInvalid(u32),
101 AddressIndexMissing,
103 AddressIndexInvalid(String),
106}
107
108impl ZcashdHdDerivation {
109 const ZCASHD_LEGACY_ACCOUNT_INDEX: u32 = 0x7FFFFFFF;
110
111 pub fn parse_hd_path<C: NetworkConstants>(
127 network: &C,
128 path: &str,
129 ) -> Result<Self, PathParseError> {
130 let re =
136 Regex::new(r"^m/32'/([0-9]+)'/([0-9]+)'(?:/([0-9]+)')?$").expect("checked to be valid");
137 let parts = re.captures(path).ok_or(PathParseError::PathInvalid)?;
138 assert!(parts.len() <= 4);
139
140 let coin_type = parts
141 .get(1)
142 .map_or(Err(PathParseError::PathInvalid), |part| {
143 let part_str = part.as_str();
144 part_str
145 .parse::<u32>()
146 .map_err(|_| PathParseError::CoinTypeInvalid(part_str.to_string()))
147 })?;
148 if coin_type != network.coin_type() {
149 return Err(PathParseError::CoinTypeMismatch {
150 expected: network.coin_type(),
151 actual: coin_type,
152 });
153 }
154
155 let account_index = parts
156 .get(2)
157 .map_or(Err(PathParseError::PathInvalid), |part| {
158 let part_str = part.as_str();
159 part_str
160 .parse::<u32>()
161 .map_err(|_| PathParseError::AccountIndexInvalid(part_str.to_string()))
162 })?;
163
164 if account_index == Self::ZCASHD_LEGACY_ACCOUNT_INDEX {
165 let address_index =
166 parts
167 .get(3)
168 .map_or(Err(PathParseError::AddressIndexMissing), |part| {
169 let part_str = part.as_str();
170
171 match part_str.parse::<u32>() {
172 Ok(v) if v < (1 << 31) => Ok(LegacyAddressIndex(v)),
173 _ => Err(PathParseError::AddressIndexInvalid(part_str.to_string())),
174 }
175 })?;
176
177 Ok(ZcashdHdDerivation::Post470LegacySapling { address_index })
178 } else {
179 if parts.get(3).is_some() {
180 return Err(PathParseError::PathInvalid);
181 }
182 let account_id = AccountId::try_from(account_index)
183 .map_err(|_| PathParseError::AccountIdInvalid(account_index))?;
184 Ok(ZcashdHdDerivation::Zip32 { account_id })
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use assert_matches::assert_matches;
192
193 use zcash_protocol::consensus::{NetworkConstants, NetworkType};
194 use zip32::AccountId;
195
196 use super::{PathParseError, ZcashdHdDerivation};
197
198 #[test]
199 fn parse_zcashd_hd_path() {
200 let zip32_bip39_path = format!("m/32'/{}'/0'", NetworkType::Main.coin_type());
201 assert_matches!(
202 ZcashdHdDerivation::parse_hd_path(&NetworkType::Main, &zip32_bip39_path),
203 Ok(ZcashdHdDerivation::Zip32 {
204 account_id: id
205 })
206 if id == AccountId::ZERO
207 );
208
209 let zip32_bip39_path_coin_invalid = format!("m/32'/{}'/0'", u64::MAX);
210 assert_matches!(
211 ZcashdHdDerivation::parse_hd_path(&NetworkType::Main, &zip32_bip39_path_coin_invalid),
212 Err(PathParseError::CoinTypeInvalid(_))
213 );
214
215 let zip32_bip39_path_coin_mismatch = format!("m/32'/{}'/0'", NetworkType::Test.coin_type());
216 assert_matches!(
217 ZcashdHdDerivation::parse_hd_path(&NetworkType::Main, &zip32_bip39_path_coin_mismatch),
218 Err(PathParseError::CoinTypeMismatch { .. })
219 );
220
221 let zip32_bip39_account_idx_invalid =
222 format!("m/32'/{}'/{}'/0'", NetworkType::Main.coin_type(), u64::MAX);
223 assert_matches!(
224 ZcashdHdDerivation::parse_hd_path(&NetworkType::Main, &zip32_bip39_account_idx_invalid),
225 Err(PathParseError::AccountIndexInvalid(_))
226 );
227
228 let zip32_bip39_account_id_invalid = format!(
229 "m/32'/{}'/{}'",
230 NetworkType::Main.coin_type(),
231 ZcashdHdDerivation::ZCASHD_LEGACY_ACCOUNT_INDEX + 1
232 );
233 assert_matches!(
234 ZcashdHdDerivation::parse_hd_path(&NetworkType::Main, &zip32_bip39_account_id_invalid),
235 Err(PathParseError::AccountIdInvalid(_))
236 );
237
238 let zcashd_legacy_path = format!(
239 "m/32'/{}'/{}'/0'",
240 NetworkType::Main.coin_type(),
241 ZcashdHdDerivation::ZCASHD_LEGACY_ACCOUNT_INDEX
242 );
243 assert_matches!(
244 ZcashdHdDerivation::parse_hd_path(&NetworkType::Main, &zcashd_legacy_path),
245 Ok(ZcashdHdDerivation::Post470LegacySapling {
246 address_index
247 })
248 if address_index.0 == 0
249 );
250
251 let too_long_path = format!("m/32'/{}'/1'/0'", NetworkType::Main.coin_type(),);
252 assert_matches!(
253 ZcashdHdDerivation::parse_hd_path(&NetworkType::Main, &too_long_path),
254 Err(PathParseError::PathInvalid)
255 );
256 }
257}