Skip to main content

aptos_sdk/account/
mnemonic.rs

1//! BIP-39 mnemonic phrase support for key derivation.
2//!
3//! This module requires the `mnemonic` feature flag.
4//!
5//! # Supported curves
6//!
7//! - **Ed25519** uses SLIP-0010 with the Aptos default path
8//!   `m/44'/637'/0'/0'/{address_index}'`. Every component MUST be hardened
9//!   because Ed25519 does not admit non-hardened child derivation (no
10//!   scalar homomorphism on Curve25519). The `from_str` parser will reject
11//!   non-hardened components when the resulting path is used with Ed25519.
12//! - **Secp256k1** uses BIP-32 with the Aptos default path
13//!   `m/44'/637'/0'/0/{address_index}` -- the last two indices are
14//!   non-hardened, matching the TypeScript SDK's `APTOS_BIP44_REGEX`.
15//!
16//! `derive_ed25519_key(index)` and `derive_secp256k1_key(index)` place
17//! `index` in the **5th (address) component**, preserving the pre-PR Rust
18//! SDK semantics so existing addresses derived from a mnemonic do not
19//! shift between SDK versions. Callers needing to vary the BIP-44
20//! **account** index (the Petra / TS-SDK convention) should build the
21//! path explicitly via [`DerivationPath::from_str`].
22
23use crate::error::{AptosError, AptosResult};
24
25/// Hardened-bit offset used by BIP-32 / SLIP-0010 to flag a hardened index.
26const HARDENED_OFFSET: u32 = 0x8000_0000;
27/// BIP-44 purpose (`44'`).
28const BIP44_PURPOSE: u32 = 44;
29/// Aptos coin type (`637'`).
30const APTOS_COIN_TYPE: u32 = 637;
31
32/// A single component of a BIP-32 derivation path.
33///
34/// Wraps a 31-bit numeric index plus a hardened flag. The encoded 32-bit
35/// value used during derivation is `index | 0x80000000` when hardened.
36///
37/// Fields are private to enforce the `index < 2^31` invariant: a value with
38/// the hardened bit already set would collide with an explicitly hardened
39/// component during BIP-32 derivation, producing an ambiguous key. Use
40/// [`Self::try_new`] to construct.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct PathComponent {
43    /// The 31-bit numeric index (without the hardened bit applied).
44    index: u32,
45    /// Whether this component is hardened (denoted by a trailing apostrophe
46    /// in path strings, e.g. `44'`).
47    hardened: bool,
48}
49
50impl PathComponent {
51    /// Constructs a path component, rejecting any `index` whose top bit is
52    /// set (i.e. `index >= 2^31 = 0x8000_0000`).
53    ///
54    /// # Errors
55    ///
56    /// Returns [`AptosError::KeyDerivation`] if `index` has its hardened
57    /// bit set; valid BIP-32 indices are confined to 31 bits and the
58    /// hardened bit is supplied via the `hardened` flag.
59    pub fn try_new(index: u32, hardened: bool) -> AptosResult<Self> {
60        if index & HARDENED_OFFSET != 0 {
61            return Err(AptosError::KeyDerivation(format!(
62                "derivation index {index} exceeds 2^31 - 1; the hardened bit \
63                 must come from the `hardened` flag, not the raw value"
64            )));
65        }
66        Ok(Self { index, hardened })
67    }
68
69    /// Returns the 31-bit numeric index without the hardened bit applied.
70    #[must_use]
71    pub fn index(self) -> u32 {
72        self.index
73    }
74
75    /// Returns `true` when this component is hardened (encoded as
76    /// `index | 0x80000000` during derivation).
77    #[must_use]
78    pub fn hardened(self) -> bool {
79        self.hardened
80    }
81
82    /// Encodes this component as the 32-bit value passed to BIP-32 / SLIP-0010
83    /// child-key derivation (i.e. with the hardened bit set when applicable).
84    #[must_use]
85    pub fn encoded(self) -> u32 {
86        if self.hardened {
87            self.index | HARDENED_OFFSET
88        } else {
89            self.index
90        }
91    }
92}
93
94/// A parsed BIP-32 / BIP-44 derivation path.
95///
96/// Use [`Self::aptos_ed25519`] / [`Self::aptos_secp256k1`] for the canonical
97/// Aptos paths, or [`Self::from_str`] to parse a custom path of the form
98/// `m/44'/637'/0'/0'/0'`.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct DerivationPath {
101    components: Vec<PathComponent>,
102}
103
104impl DerivationPath {
105    /// Returns the path components in order.
106    #[must_use]
107    pub fn components(&self) -> &[PathComponent] {
108        &self.components
109    }
110
111    /// Returns `true` iff every component in the path is hardened.
112    ///
113    /// Ed25519 derivation rejects paths that are not fully hardened.
114    #[must_use]
115    pub fn is_fully_hardened(&self) -> bool {
116        self.components.iter().all(|c| c.hardened())
117    }
118
119    /// Builds the canonical Aptos Ed25519 derivation path
120    /// `m/44'/637'/0'/0'/{address_index}'`.
121    ///
122    /// The `address_index` is placed in the final (address) BIP-44 component
123    /// for backward compatibility with the pre-existing
124    /// `Mnemonic::derive_ed25519_key(index)` behavior. Callers needing to
125    /// vary the BIP-44 *account* index (the Petra/TS-SDK convention) should
126    /// construct the path explicitly via [`Self::from_str`] and pass it to
127    /// [`Mnemonic::derive_ed25519_key_at_path`].
128    ///
129    /// All five components are hardened, matching the TypeScript SDK's
130    /// `APTOS_HARDENED_REGEX`.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`AptosError::KeyDerivation`] if `address_index >= 2^31`
135    /// (the top bit is reserved as the BIP-32 hardened flag).
136    pub fn aptos_ed25519(address_index: u32) -> AptosResult<Self> {
137        let h = |i| PathComponent::try_new(i, true);
138        Ok(Self {
139            components: vec![
140                h(BIP44_PURPOSE)?,
141                h(APTOS_COIN_TYPE)?,
142                h(0)?,
143                h(0)?,
144                h(address_index)?,
145            ],
146        })
147    }
148
149    /// Builds the canonical Aptos Secp256k1 derivation path
150    /// `m/44'/637'/0'/0/{address_index}`.
151    ///
152    /// The `address_index` is placed in the final (address) BIP-44 component
153    /// for symmetry with [`Self::aptos_ed25519`]. The last two indices are
154    /// non-hardened, matching the TypeScript SDK's `APTOS_BIP44_REGEX`.
155    /// Callers needing to vary the BIP-44 *account* index should construct
156    /// the path explicitly via [`Self::from_str`].
157    ///
158    /// # Errors
159    ///
160    /// Returns [`AptosError::KeyDerivation`] if `address_index >= 2^31`.
161    pub fn aptos_secp256k1(address_index: u32) -> AptosResult<Self> {
162        let h = |i| PathComponent::try_new(i, true);
163        let u = |i| PathComponent::try_new(i, false);
164        Ok(Self {
165            components: vec![
166                h(BIP44_PURPOSE)?,
167                h(APTOS_COIN_TYPE)?,
168                h(0)?,
169                u(0)?,
170                u(address_index)?,
171            ],
172        })
173    }
174
175    /// Parses a derivation path of the form `m/44'/637'/0'/0'/0'`.
176    ///
177    /// Apostrophes (`'`) and lowercase `h` are accepted as hardened markers.
178    /// The leading `m/` (or `M/`) prefix is required.
179    ///
180    /// This is a convenience wrapper around the [`std::str::FromStr`]
181    /// implementation; both produce identical results.
182    ///
183    /// # Errors
184    ///
185    /// Returns [`AptosError::KeyDerivation`] if the path is malformed, a
186    /// component is missing, the numeric value exceeds 2^31 - 1, or the
187    /// path is empty.
188    #[allow(clippy::should_implement_trait)] // also implemented via FromStr below; inherent method kept for ergonomics so callers don't need a `use std::str::FromStr` import.
189    pub fn from_str(path: &str) -> AptosResult<Self> {
190        <Self as std::str::FromStr>::from_str(path)
191    }
192}
193
194impl std::str::FromStr for DerivationPath {
195    type Err = AptosError;
196
197    fn from_str(path: &str) -> AptosResult<Self> {
198        let mut parts = path.split('/');
199        let head = parts
200            .next()
201            .ok_or_else(|| AptosError::KeyDerivation("empty derivation path".to_string()))?;
202        if !matches!(head, "m" | "M") {
203            return Err(AptosError::KeyDerivation(format!(
204                "derivation path must start with 'm/', got: {path}"
205            )));
206        }
207
208        let mut components = Vec::new();
209        for raw in parts {
210            if raw.is_empty() {
211                return Err(AptosError::KeyDerivation(format!(
212                    "empty component in derivation path: {path}"
213                )));
214            }
215            let (digits, hardened) = if let Some(rest) = raw.strip_suffix('\'') {
216                (rest, true)
217            } else if let Some(rest) = raw.strip_suffix('h') {
218                (rest, true)
219            } else {
220                (raw, false)
221            };
222
223            let index: u32 = digits.parse().map_err(|_| {
224                AptosError::KeyDerivation(format!(
225                    "invalid numeric component '{raw}' in derivation path: {path}"
226                ))
227            })?;
228
229            components.push(
230                PathComponent::try_new(index, hardened).map_err(|e| match e {
231                    AptosError::KeyDerivation(msg) => {
232                        AptosError::KeyDerivation(format!("{msg} in path: {path}"))
233                    }
234                    other => other,
235                })?,
236            );
237        }
238
239        if components.is_empty() {
240            return Err(AptosError::KeyDerivation(format!(
241                "derivation path has no components: {path}"
242            )));
243        }
244
245        Ok(Self { components })
246    }
247}
248
249impl std::fmt::Display for DerivationPath {
250    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251        f.write_str("m")?;
252        for c in &self.components {
253            if c.hardened() {
254                write!(f, "/{}'", c.index())?;
255            } else {
256                write!(f, "/{}", c.index())?;
257            }
258        }
259        Ok(())
260    }
261}
262
263/// A BIP-39 mnemonic phrase for key derivation.
264///
265/// # Example
266///
267/// ```rust
268/// use aptos_sdk::account::Mnemonic;
269///
270/// // Generate a new mnemonic
271/// let mnemonic = Mnemonic::generate(24).unwrap();
272/// println!("Mnemonic: {}", mnemonic.phrase());
273///
274/// // Parse an existing mnemonic
275/// let mnemonic = Mnemonic::from_phrase("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap();
276/// ```
277#[derive(Clone)]
278pub struct Mnemonic {
279    phrase: String,
280}
281
282impl Mnemonic {
283    /// Generates a new random mnemonic phrase.
284    ///
285    /// # Arguments
286    ///
287    /// * `word_count` - Number of words (12, 15, 18, 21, or 24)
288    ///
289    /// # Errors
290    ///
291    /// Returns an error if the word count is not one of 12, 15, 18, 21, or 24,
292    /// or if entropy generation fails.
293    pub fn generate(word_count: usize) -> AptosResult<Self> {
294        let entropy_bytes = match word_count {
295            12 => 16, // 128 bits
296            15 => 20, // 160 bits
297            18 => 24, // 192 bits
298            21 => 28, // 224 bits
299            24 => 32, // 256 bits
300            _ => {
301                return Err(AptosError::InvalidMnemonic(format!(
302                    "invalid word count: {word_count}, must be 12, 15, 18, 21, or 24"
303                )));
304            }
305        };
306
307        let mut entropy = vec![0u8; entropy_bytes];
308        rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut entropy);
309
310        let mnemonic = bip39::Mnemonic::from_entropy(&entropy)
311            .map_err(|e| AptosError::InvalidMnemonic(e.to_string()));
312
313        // SECURITY: Zeroize entropy before it goes out of scope to prevent
314        // key material from lingering in memory
315        zeroize::Zeroize::zeroize(&mut entropy);
316
317        let mnemonic = mnemonic?;
318
319        Ok(Self {
320            phrase: mnemonic.to_string(),
321        })
322    }
323
324    /// Creates a mnemonic from an existing phrase.
325    ///
326    /// # Errors
327    ///
328    /// Returns an error if the phrase is not a valid BIP-39 mnemonic.
329    pub fn from_phrase(phrase: &str) -> AptosResult<Self> {
330        // Validate the mnemonic
331        let _mnemonic = bip39::Mnemonic::parse_normalized(phrase)
332            .map_err(|e| AptosError::InvalidMnemonic(e.to_string()))?;
333
334        Ok(Self {
335            phrase: phrase.to_string(),
336        })
337    }
338
339    /// Returns the mnemonic phrase.
340    pub fn phrase(&self) -> &str {
341        &self.phrase
342    }
343
344    /// Derives the seed from this mnemonic.
345    ///
346    /// Uses an empty passphrase by default.
347    ///
348    /// # Errors
349    ///
350    /// Returns an error if the mnemonic cannot be re-parsed (should not happen
351    /// since the phrase was validated during construction).
352    pub fn to_seed(&self) -> AptosResult<[u8; 64]> {
353        self.to_seed_with_passphrase("")
354    }
355
356    /// Derives the seed from this mnemonic with a passphrase.
357    ///
358    /// # Errors
359    ///
360    /// Returns an error if the mnemonic phrase cannot be re-parsed. This should
361    /// never happen because the phrase is validated during construction, but
362    /// returning an error is safer than panicking.
363    pub fn to_seed_with_passphrase(&self, passphrase: &str) -> AptosResult<[u8; 64]> {
364        let mnemonic = bip39::Mnemonic::parse_normalized(&self.phrase).map_err(|e| {
365            AptosError::InvalidMnemonic(format!("internal error: mnemonic re-parse failed: {e}"))
366        })?;
367
368        Ok(mnemonic.to_seed(passphrase))
369    }
370
371    /// Derives an Ed25519 private key using the Aptos default path
372    /// `m/44'/637'/0'/0'/{address_index}'`, where `index` selects the
373    /// 5th (address) component. Index `0` yields `m/44'/637'/0'/0'/0'`.
374    ///
375    /// # Errors
376    ///
377    /// Returns an error if key derivation fails or the derived key is invalid.
378    #[cfg(feature = "ed25519")]
379    pub fn derive_ed25519_key(&self, index: u32) -> AptosResult<crate::crypto::Ed25519PrivateKey> {
380        self.derive_ed25519_key_at_path(&DerivationPath::aptos_ed25519(index)?)
381    }
382
383    /// Derives an Ed25519 private key at a custom BIP-44 path.
384    ///
385    /// All components of `path` must be hardened, matching SLIP-0010 (Ed25519
386    /// has no non-hardened child derivation).
387    ///
388    /// # Errors
389    ///
390    /// Returns an error if the path contains a non-hardened component, if
391    /// HMAC/SLIP-0010 derivation fails, or if the resulting key bytes do not
392    /// form a valid Ed25519 private key.
393    #[cfg(feature = "ed25519")]
394    pub fn derive_ed25519_key_at_path(
395        &self,
396        path: &DerivationPath,
397    ) -> AptosResult<crate::crypto::Ed25519PrivateKey> {
398        if !path.is_fully_hardened() {
399            return Err(AptosError::KeyDerivation(format!(
400                "Ed25519 derivation requires every path component to be hardened; got {path}"
401            )));
402        }
403
404        let mut seed = self.to_seed()?;
405        let result = derive_ed25519_at_path(&seed, path);
406        // SECURITY: Zeroize seed after use
407        zeroize::Zeroize::zeroize(&mut seed);
408        let mut key = result?;
409        let private_key = crate::crypto::Ed25519PrivateKey::from_bytes(&key);
410        // SECURITY: Zeroize raw key bytes after creating the key object
411        zeroize::Zeroize::zeroize(&mut key);
412        private_key
413    }
414
415    /// Derives a Secp256k1 private key using the Aptos default path
416    /// `m/44'/637'/0'/0/{address_index}`, where `index` selects the 5th
417    /// (address) component. Index `0` yields `m/44'/637'/0'/0/0`.
418    ///
419    /// The last two indices are non-hardened by convention, matching the
420    /// TypeScript SDK.
421    ///
422    /// # Errors
423    ///
424    /// Returns an error if key derivation fails or the derived scalar is
425    /// invalid (probability ~2^-127 per step, effectively never).
426    #[cfg(feature = "secp256k1")]
427    pub fn derive_secp256k1_key(
428        &self,
429        index: u32,
430    ) -> AptosResult<crate::crypto::Secp256k1PrivateKey> {
431        self.derive_secp256k1_key_at_path(&DerivationPath::aptos_secp256k1(index)?)
432    }
433
434    /// Derives a Secp256k1 private key at a custom BIP-32 path.
435    ///
436    /// Supports hardened and non-hardened components per BIP-32.
437    ///
438    /// # Errors
439    ///
440    /// Returns an error if HMAC fails, if any intermediate scalar is invalid
441    /// (zero or >= curve order -- vanishingly rare), or if the final 32 bytes
442    /// do not form a valid Secp256k1 private key.
443    #[cfg(feature = "secp256k1")]
444    pub fn derive_secp256k1_key_at_path(
445        &self,
446        path: &DerivationPath,
447    ) -> AptosResult<crate::crypto::Secp256k1PrivateKey> {
448        let mut seed = self.to_seed()?;
449        let result = derive_secp256k1_at_path(&seed, path);
450        zeroize::Zeroize::zeroize(&mut seed);
451        let mut bytes = result?;
452        let key = crate::crypto::Secp256k1PrivateKey::from_bytes(&bytes);
453        zeroize::Zeroize::zeroize(&mut bytes);
454        key
455    }
456}
457
458/// Derives an Ed25519 key from a seed using SLIP-0010 along the given path.
459#[cfg(feature = "ed25519")]
460fn derive_ed25519_at_path(seed: &[u8], path: &DerivationPath) -> AptosResult<[u8; 32]> {
461    use hmac::{Hmac, Mac};
462    use sha2::Sha512;
463
464    type HmacSha512 = Hmac<Sha512>;
465
466    // SLIP-0010 master key derivation
467    let mut mac = HmacSha512::new_from_slice(b"ed25519 seed")
468        .map_err(|e| AptosError::KeyDerivation(e.to_string()))?;
469    mac.update(seed);
470    let result = mac.finalize().into_bytes();
471
472    let mut key = [0u8; 32];
473    let mut chain_code = [0u8; 32];
474    key.copy_from_slice(&result[..32]);
475    chain_code.copy_from_slice(&result[32..]);
476
477    for component in path.components() {
478        let mut data = vec![0u8];
479        data.extend_from_slice(&key);
480        data.extend_from_slice(&component.encoded().to_be_bytes());
481
482        let mut mac = HmacSha512::new_from_slice(&chain_code)
483            .map_err(|e| AptosError::KeyDerivation(e.to_string()))?;
484        mac.update(&data);
485        let result = mac.finalize().into_bytes();
486
487        key.copy_from_slice(&result[..32]);
488        chain_code.copy_from_slice(&result[32..]);
489
490        // SECURITY: Zeroize intermediate derivation data
491        zeroize::Zeroize::zeroize(&mut data);
492    }
493
494    // SECURITY: Zeroize chain_code since we only return the key
495    zeroize::Zeroize::zeroize(&mut chain_code);
496
497    Ok(key)
498}
499
500/// Derives a Secp256k1 private key from a seed using BIP-32 along `path`.
501///
502/// Supports both hardened and non-hardened components. For non-hardened
503/// derivation the parent compressed public key is fed into HMAC; the child
504/// scalar is `(I_L + k_par) mod n`.
505#[cfg(feature = "secp256k1")]
506fn derive_secp256k1_at_path(seed: &[u8], path: &DerivationPath) -> AptosResult<[u8; 32]> {
507    use hmac::{Hmac, Mac};
508    use k256::elliptic_curve::sec1::ToEncodedPoint;
509    use k256::{NonZeroScalar, ProjectivePoint, PublicKey, Scalar, SecretKey};
510    use sha2::Sha512;
511
512    type HmacSha512 = Hmac<Sha512>;
513
514    // BIP-32 master key derivation: HMAC-SHA512(key = "Bitcoin seed", seed)
515    let mut mac = HmacSha512::new_from_slice(b"Bitcoin seed")
516        .map_err(|e| AptosError::KeyDerivation(e.to_string()))?;
517    mac.update(seed);
518    let result = mac.finalize().into_bytes();
519
520    let mut key_bytes = [0u8; 32];
521    let mut chain_code = [0u8; 32];
522    key_bytes.copy_from_slice(&result[..32]);
523    chain_code.copy_from_slice(&result[32..]);
524
525    // Validate master key forms a valid scalar.
526    let mut parent = SecretKey::from_slice(&key_bytes)
527        .map_err(|e| AptosError::KeyDerivation(format!("invalid master scalar: {e}")))?;
528
529    for component in path.components() {
530        let encoded = component.encoded();
531        let (mut data, hardened) = if component.hardened() {
532            // 0x00 || ser_256(k_par) || ser_32(i)  -- contains parent secret
533            let mut buf = Vec::with_capacity(1 + 32 + 4);
534            buf.push(0u8);
535            buf.extend_from_slice(&parent.to_bytes());
536            buf.extend_from_slice(&encoded.to_be_bytes());
537            (buf, true)
538        } else {
539            // ser_P(K_par) || ser_32(i)  -- compressed public key (33 bytes)
540            let pub_key: PublicKey = parent.public_key();
541            let encoded_point = pub_key.to_encoded_point(true);
542            let mut buf = Vec::with_capacity(33 + 4);
543            buf.extend_from_slice(encoded_point.as_bytes());
544            buf.extend_from_slice(&encoded.to_be_bytes());
545            (buf, false)
546        };
547
548        let mut mac = HmacSha512::new_from_slice(&chain_code)
549            .map_err(|e| AptosError::KeyDerivation(e.to_string()))?;
550        mac.update(&data);
551        let result = mac.finalize().into_bytes();
552        if hardened {
553            // SECURITY: data contained the parent private key for hardened
554            // derivation; zeroize before dropping.
555            zeroize::Zeroize::zeroize(&mut data);
556        }
557
558        // BIP-32 spec note: if I_L >= n or the derived child scalar is zero,
559        // the spec recommends proceeding with index i+1. We deliberately
560        // error instead, matching the Rust `bip32` crate: silently advancing
561        // the index would change the path the caller asked for, and the
562        // probability of either failure is ~2^-127 per component. Callers
563        // that hit this can pick a different address index explicitly.
564        let il_scalar = NonZeroScalar::try_from(&result[..32]).map_err(|e| {
565            AptosError::KeyDerivation(format!(
566                "BIP-32 derivation produced invalid intermediate scalar: {e}"
567            ))
568        })?;
569
570        // child_scalar = I_L + k_par (mod n)
571        let parent_scalar: Scalar = *parent.to_nonzero_scalar().as_ref();
572        let child_scalar = *il_scalar.as_ref() + parent_scalar;
573
574        let child_nz =
575            Option::<NonZeroScalar>::from(NonZeroScalar::new(child_scalar)).ok_or_else(|| {
576                AptosError::KeyDerivation(
577                    "BIP-32 derivation produced zero child scalar".to_string(),
578                )
579            })?;
580        parent = SecretKey::from(child_nz);
581
582        // Sanity: the child public key must be on the curve (it is, by
583        // construction, since child_scalar is a non-zero scalar). The
584        // projective-point form is computed implicitly by `parent.public_key()`
585        // on the next iteration. We do not need to re-derive it here, but the
586        // reference is kept so future changes don't accidentally drop it.
587        let _ = ProjectivePoint::GENERATOR;
588
589        // Update chain code from I_R.
590        chain_code.copy_from_slice(&result[32..]);
591    }
592
593    let mut out = [0u8; 32];
594    out.copy_from_slice(&parent.to_bytes());
595
596    // SECURITY: Zeroize transient buffers.
597    zeroize::Zeroize::zeroize(&mut key_bytes);
598    zeroize::Zeroize::zeroize(&mut chain_code);
599
600    Ok(out)
601}
602
603impl std::fmt::Debug for Mnemonic {
604    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
605        write!(f, "Mnemonic([REDACTED])")
606    }
607}
608
609#[cfg(test)]
610mod tests {
611    use super::*;
612
613    const TEST_PHRASE: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
614
615    #[test]
616    fn test_generate_mnemonic() {
617        let mnemonic = Mnemonic::generate(12).unwrap();
618        assert_eq!(mnemonic.phrase().split_whitespace().count(), 12);
619
620        let mnemonic = Mnemonic::generate(24).unwrap();
621        assert_eq!(mnemonic.phrase().split_whitespace().count(), 24);
622    }
623
624    #[test]
625    fn test_invalid_word_count() {
626        assert!(Mnemonic::generate(13).is_err());
627    }
628
629    #[test]
630    fn test_parse_mnemonic() {
631        let mnemonic = Mnemonic::from_phrase(TEST_PHRASE).unwrap();
632        assert_eq!(mnemonic.phrase(), TEST_PHRASE);
633    }
634
635    #[test]
636    fn test_invalid_mnemonic() {
637        assert!(Mnemonic::from_phrase("invalid mnemonic phrase").is_err());
638    }
639
640    #[test]
641    fn test_path_from_str_hardened() {
642        let path = DerivationPath::from_str("m/44'/637'/0'/0'/0'").unwrap();
643        assert!(path.is_fully_hardened());
644        assert_eq!(path.components().len(), 5);
645        assert_eq!(path.to_string(), "m/44'/637'/0'/0'/0'");
646    }
647
648    #[test]
649    fn test_path_from_str_mixed() {
650        let path = DerivationPath::from_str("m/44'/637'/0'/0/0").unwrap();
651        assert!(!path.is_fully_hardened());
652        let comps = path.components();
653        assert!(comps[0].hardened && comps[1].hardened && comps[2].hardened);
654        assert!(!comps[3].hardened && !comps[4].hardened);
655    }
656
657    #[test]
658    fn test_path_from_str_h_marker() {
659        // Lowercase `h` is also a hardened marker.
660        let path = DerivationPath::from_str("m/44h/637h/0h").unwrap();
661        assert!(path.is_fully_hardened());
662    }
663
664    #[test]
665    fn test_path_from_str_rejects_bad_prefix() {
666        assert!(DerivationPath::from_str("44'/637'").is_err());
667        assert!(DerivationPath::from_str("").is_err());
668        assert!(DerivationPath::from_str("m").is_err());
669        assert!(DerivationPath::from_str("m/44'/abc/0").is_err());
670    }
671
672    #[test]
673    fn test_path_from_str_rejects_oversize_index() {
674        // 2^31 sets the hardened bit -- reject it as a raw numeric value.
675        assert!(DerivationPath::from_str("m/2147483648").is_err());
676    }
677
678    #[test]
679    fn test_aptos_default_paths() {
680        // `address_index` lives in the 5th BIP-44 component, matching the
681        // pre-PR `derive_ed25519_key(index)` behavior so existing addresses
682        // derived from a mnemonic do not silently shift between SDK versions.
683        assert_eq!(
684            DerivationPath::aptos_ed25519(0).unwrap().to_string(),
685            "m/44'/637'/0'/0'/0'"
686        );
687        assert_eq!(
688            DerivationPath::aptos_secp256k1(0).unwrap().to_string(),
689            "m/44'/637'/0'/0/0"
690        );
691        assert_eq!(
692            DerivationPath::aptos_ed25519(5).unwrap().to_string(),
693            "m/44'/637'/0'/0'/5'",
694            "address_index belongs in the 5th component",
695        );
696        assert_eq!(
697            DerivationPath::aptos_secp256k1(5).unwrap().to_string(),
698            "m/44'/637'/0'/0/5",
699            "address_index belongs in the 5th component (non-hardened)",
700        );
701    }
702
703    #[test]
704    fn test_aptos_default_paths_reject_oversize_index() {
705        // The 31-bit invariant surfaces as an error rather than panicking.
706        assert!(DerivationPath::aptos_ed25519(0x8000_0000).is_err());
707        assert!(DerivationPath::aptos_secp256k1(0x8000_0000).is_err());
708    }
709
710    #[test]
711    #[cfg(feature = "ed25519")]
712    fn test_derive_ed25519_key() {
713        let mnemonic = Mnemonic::from_phrase(TEST_PHRASE).unwrap();
714
715        let key1 = mnemonic.derive_ed25519_key(0).unwrap();
716        let key2 = mnemonic.derive_ed25519_key(0).unwrap();
717        assert_eq!(key1.to_bytes(), key2.to_bytes());
718
719        let key3 = mnemonic.derive_ed25519_key(1).unwrap();
720        assert_ne!(key1.to_bytes(), key3.to_bytes());
721    }
722
723    #[test]
724    #[cfg(feature = "ed25519")]
725    fn test_derive_ed25519_at_path_rejects_unhardened() {
726        let mnemonic = Mnemonic::from_phrase(TEST_PHRASE).unwrap();
727        let path = DerivationPath::from_str("m/44'/637'/0'/0/0").unwrap();
728        let err = mnemonic.derive_ed25519_key_at_path(&path).unwrap_err();
729        assert!(matches!(err, AptosError::KeyDerivation(_)));
730    }
731
732    #[test]
733    #[cfg(feature = "ed25519")]
734    fn test_derive_ed25519_default_matches_path() {
735        let mnemonic = Mnemonic::from_phrase(TEST_PHRASE).unwrap();
736        let via_index = mnemonic.derive_ed25519_key(3).unwrap();
737        let via_path = mnemonic
738            .derive_ed25519_key_at_path(&DerivationPath::aptos_ed25519(3).unwrap())
739            .unwrap();
740        assert_eq!(via_index.to_bytes(), via_path.to_bytes());
741    }
742
743    #[test]
744    #[cfg(feature = "secp256k1")]
745    fn test_derive_secp256k1_deterministic() {
746        let mnemonic = Mnemonic::from_phrase(TEST_PHRASE).unwrap();
747        let key1 = mnemonic.derive_secp256k1_key(0).unwrap();
748        let key2 = mnemonic.derive_secp256k1_key(0).unwrap();
749        assert_eq!(key1.to_bytes(), key2.to_bytes());
750
751        let key3 = mnemonic.derive_secp256k1_key(1).unwrap();
752        assert_ne!(key1.to_bytes(), key3.to_bytes());
753    }
754
755    /// Cross-SDK regression: the abandon-test mnemonic derived at the
756    /// canonical Aptos Secp256k1 path `m/44'/637'/0'/0/0` must produce this
757    /// exact private key. Changes to BIP-32 derivation that move this byte
758    /// sequence are silent regressions vs the TypeScript SDK and MUST be
759    /// cross-checked there before updating the fixture.
760    #[test]
761    #[cfg(feature = "secp256k1")]
762    fn test_derive_secp256k1_pinned_aptos_vector() {
763        let mnemonic = Mnemonic::from_phrase(TEST_PHRASE).unwrap();
764        let key = mnemonic.derive_secp256k1_key(0).unwrap();
765        assert_eq!(
766            const_hex::encode(key.to_bytes()),
767            "4613c3acaffc152273c102a6b27f6f4209e1d54cac18ad0ac96b5892b7d7bf91",
768        );
769    }
770
771    /// Cross-validates the BIP-32 implementation against the well-known
772    /// Bitcoin reference vector: the abandon-test mnemonic at
773    /// `m/44'/0'/0'/0/0` (coin type 0, not Aptos) must yield the canonical
774    /// Bitcoin private key. Verifies HMAC master derivation, hardened
775    /// derivation, and non-hardened (public-key-based) derivation in a
776    /// single fixture. Source: any reputable BIP-32 / mnemonic-to-key
777    /// reference for the all-zero entropy mnemonic.
778    #[test]
779    #[cfg(feature = "secp256k1")]
780    fn test_derive_secp256k1_bitcoin_reference_vector() {
781        let mnemonic = Mnemonic::from_phrase(TEST_PHRASE).unwrap();
782        let path = DerivationPath::from_str("m/44'/0'/0'/0/0").unwrap();
783        let key = mnemonic.derive_secp256k1_key_at_path(&path).unwrap();
784        assert_eq!(
785            const_hex::encode(key.to_bytes()),
786            "e284129cc0922579a535bbf4d1a3b25773090d28c909bc0fed73b5e0222cc372",
787        );
788    }
789
790    #[test]
791    fn test_path_component_encoded_sets_hardened_bit() {
792        let hardened = PathComponent::try_new(44, true).unwrap();
793        let unhardened = PathComponent::try_new(44, false).unwrap();
794        assert_eq!(hardened.encoded(), 0x8000_002C);
795        assert_eq!(unhardened.encoded(), 44);
796        assert_eq!(hardened.index(), 44);
797        assert!(hardened.hardened());
798        assert!(!unhardened.hardened());
799    }
800
801    #[test]
802    fn test_path_component_rejects_oversize_index() {
803        // Top bit reserved as the hardened flag in BIP-32; raw values
804        // with that bit already set would collide with hardened components
805        // and produce ambiguous derivations. `try_new` MUST reject them.
806        assert!(PathComponent::try_new(0x8000_0000, false).is_err());
807        assert!(PathComponent::try_new(0x8000_0000, true).is_err());
808        assert!(PathComponent::try_new(0xFFFF_FFFF, false).is_err());
809        // Boundary: 2^31 - 1 is the largest valid raw index.
810        assert!(PathComponent::try_new(0x7FFF_FFFF, true).is_ok());
811    }
812
813    #[test]
814    fn test_path_display_roundtrip() {
815        for s in ["m/44'/637'/0'/0/0", "m/44'/637'/3'/0'/0'", "m/0"] {
816            let path = DerivationPath::from_str(s).unwrap();
817            assert_eq!(path.to_string(), s, "roundtrip drifted for {s}");
818        }
819    }
820
821    #[test]
822    fn test_path_from_str_via_parse_trait() {
823        // The FromStr trait impl exists so callers can `path.parse()` --
824        // verify it returns the same shape as the inherent constructor.
825        use std::str::FromStr;
826        let via_inherent = DerivationPath::from_str("m/44'/637'/0'/0/0").unwrap();
827        let via_trait: DerivationPath = "m/44'/637'/0'/0/0".parse().unwrap();
828        let via_fromstr = <DerivationPath as FromStr>::from_str("m/44'/637'/0'/0/0").unwrap();
829        assert_eq!(via_inherent, via_trait);
830        assert_eq!(via_inherent, via_fromstr);
831    }
832
833    #[test]
834    fn test_path_from_str_rejects_empty_component() {
835        // Double slash should not be silently absorbed into a single segment.
836        assert!(DerivationPath::from_str("m//44'").is_err());
837        assert!(DerivationPath::from_str("m/44'/").is_err());
838    }
839
840    #[test]
841    #[cfg(feature = "ed25519")]
842    fn test_passphrase_changes_derived_key() {
843        // BIP-39 mandates the passphrase be folded into the seed; verify
844        // the derived key is sensitive to it (otherwise the "secret
845        // passphrase" feature would be a no-op). We compare derived key
846        // bytes directly rather than just the seeds.
847        let mnemonic = Mnemonic::from_phrase(TEST_PHRASE).unwrap();
848        let seed_default = mnemonic.to_seed().unwrap();
849        let seed_passphrase = mnemonic.to_seed_with_passphrase("hunter2").unwrap();
850        assert_ne!(seed_default, seed_passphrase);
851
852        let path = DerivationPath::aptos_ed25519(0).unwrap();
853        let key_default = derive_ed25519_at_path(&seed_default, &path).unwrap();
854        let key_passphrase = derive_ed25519_at_path(&seed_passphrase, &path).unwrap();
855        assert_ne!(
856            key_default, key_passphrase,
857            "BIP-39 passphrase must produce a distinct derived key"
858        );
859    }
860
861    #[test]
862    #[cfg(feature = "secp256k1")]
863    fn test_derive_secp256k1_different_paths_produce_different_keys() {
864        // Sanity: distinct paths must produce distinct keys (otherwise
865        // a derivation bug could be hiding behind a single-path test).
866        let m = Mnemonic::from_phrase(TEST_PHRASE).unwrap();
867        let k0 = m.derive_secp256k1_key(0).unwrap();
868        let k1 = m.derive_secp256k1_key(1).unwrap();
869        let k_bitcoin = m
870            .derive_secp256k1_key_at_path(&DerivationPath::from_str("m/44'/0'/0'/0/0").unwrap())
871            .unwrap();
872        assert_ne!(k0.to_bytes(), k1.to_bytes());
873        assert_ne!(k0.to_bytes(), k_bitcoin.to_bytes());
874        assert_ne!(k1.to_bytes(), k_bitcoin.to_bytes());
875    }
876
877    #[test]
878    #[cfg(feature = "secp256k1")]
879    fn test_derive_secp256k1_custom_path() {
880        let mnemonic = Mnemonic::from_phrase(TEST_PHRASE).unwrap();
881        let path = DerivationPath::from_str("m/44'/637'/0'/0/0").unwrap();
882        let via_path = mnemonic.derive_secp256k1_key_at_path(&path).unwrap();
883        let via_index = mnemonic.derive_secp256k1_key(0).unwrap();
884        assert_eq!(via_path.to_bytes(), via_index.to_bytes());
885    }
886}