From ac06cb6f718f88f406602f6bab63eeef47719364 Mon Sep 17 00:00:00 2001 From: Steven Gu Date: Tue, 20 Sep 2022 15:23:22 +0800 Subject: [PATCH] Implement recovering account from derive path. --- Cargo.lock | 50 ++++++++++++++++++++++++++++++++++++++++++++++ sdk/Cargo.toml | 2 ++ sdk/src/types.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index a342a38531dd1..90a5cd6958b79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1183,11 +1183,13 @@ dependencies = [ "aptos-types", "bcs 0.1.3 (git+https://github.com/aptos-labs/bcs?rev=2cde3e8446c460cb17b0c1d6bac7e27e964ac169)", "cached-packages", + "ed25519-dalek-bip32", "move-core-types", "once_cell", "rand 0.7.3", "rand_core 0.5.1", "serde 1.0.144", + "tiny-bip39", "tokio", "url", ] @@ -3261,6 +3263,12 @@ dependencies = [ "structopt", ] +[[package]] +name = "derivation-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" + [[package]] name = "derive_arbitrary" version = "1.1.6" @@ -3496,6 +3504,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519-dalek-bip32" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2be62a4061b872c8c0873ee4fc6f101ce7b889d039f019c5fa2af471a59908" +dependencies = [ + "derivation-path", + "ed25519-dalek", + "hmac 0.12.1", + "sha2 0.10.2", +] + [[package]] name = "ed25519-dalek-fiat" version = "0.1.0" @@ -4251,8 +4271,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -6910,6 +6932,15 @@ dependencies = [ "serde 1.0.144", ] +[[package]] +name = "pbkdf2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" +dependencies = [ + "crypto-mac 0.8.0", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -9345,6 +9376,25 @@ dependencies = [ "lazy_static 0.2.11", ] +[[package]] +name = "tiny-bip39" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" +dependencies = [ + "anyhow", + "hmac 0.8.1", + "once_cell", + "pbkdf2", + "rand 0.7.3", + "rustc-hash", + "sha2 0.9.9", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + [[package]] name = "tiny-keccak" version = "2.0.2" diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 16959bf6dd53e..d67c971b06258 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -20,9 +20,11 @@ aptos-rest-client = { path = "../crates/aptos-rest-client" } aptos-types = { path = "../types" } bcs = { git = "https://github.com/aptos-labs/bcs", rev = "2cde3e8446c460cb17b0c1d6bac7e27e964ac169" } cached-packages = { path = "../aptos-move/framework/cached-packages" } +ed25519-dalek-bip32 = "0.2.0" move-core-types = { workspace = true } rand_core = "0.5.1" serde = { version = "1.0.137", features = ["derive"] } +tiny-bip39 = "0.8.2" # Used by the examples. [dev-dependencies] diff --git a/sdk/src/types.rs b/sdk/src/types.rs index f28c669e44dd7..ae72e4a0985d7 100644 --- a/sdk/src/types.rs +++ b/sdk/src/types.rs @@ -13,8 +13,12 @@ use crate::{ }, }; +use anyhow::Result; use aptos_types::event::EventKey; pub use aptos_types::*; +use bip39::{Language, Mnemonic, Seed}; +use ed25519_dalek_bip32::{DerivationPath, ExtendedSecretKey}; +use std::str::FromStr; /// LocalAccount represents an account on the Aptos blockchain. Internally it /// holds the private / public key pair and the address of the account. You can @@ -42,6 +46,29 @@ impl LocalAccount { } } + /// Recover an account from derive path (e.g. m/44'/637'/0'/0'/0') and mnemonic phrase, + pub fn from_derive_path( + derive_path: &str, + mnemonic_phrase: &str, + sequence_number: u64, + ) -> Result { + let derive_path = DerivationPath::from_str(derive_path)?; + let mnemonic = Mnemonic::from_phrase(mnemonic_phrase, Language::English)?; + // TODO: Make `password` as an optional argument. + let seed = Seed::new(&mnemonic, ""); + let key = ExtendedSecretKey::from_seed(seed.as_bytes())? + .derive(&derive_path)? + .secret_key; + let key = AccountKey::from(Ed25519PrivateKey::try_from(key.as_bytes().as_ref())?); + let address = key.authentication_key().derived_address(); + + Ok(Self { + address, + key, + sequence_number, + }) + } + /// Generate a new account locally. Note: This function does not actually /// create an account on the Aptos blockchain, it just generates a new /// account locally. @@ -183,3 +210,28 @@ impl From for AccountKey { Self::from_private_key(private_key) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_recover_account_from_derive_path() { + // Same constants in test cases of TypeScript + // https://github.com/aptos-labs/aptos-core/blob/main/ecosystem/typescript/sdk/src/aptos_account.test.ts + let derive_path = "m/44'/637'/0'/0'/0'"; + let mnemonic_phrase = + "shoot island position soft burden budget tooth cruel issue economy destroy above"; + let expected_address = "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30"; + + // Validate if the expected address. + let account = LocalAccount::from_derive_path(derive_path, mnemonic_phrase, 0).unwrap(); + assert_eq!(account.address().to_hex_literal(), expected_address); + + // Return an error for empty derive path. + assert!(LocalAccount::from_derive_path("", mnemonic_phrase, 0).is_err()); + + // Return an error for empty mnemonic phrase. + assert!(LocalAccount::from_derive_path(derive_path, "", 0).is_err()); + } +}