介紹#
讓我們回憶一下,你如何開始你的以太坊之旅的?安裝 Metamask 插件,創建助記詞,將它抄寫到一張紙上,然後點擊確認按鈕! 恭喜你有了自己的以太坊賬戶,你可以複製你的錢包地址並將其發送給其他人,讓他們用它來接收以太幣。
但是你是否有這樣的問題?
- 可以從詞典中創建自己的助記詞嗎?
- 為什麼助記詞很重要?為什麼我不能把我的助記詞送給別人?
- 我可以用我的 ETH 私鑰生成我的 BTC 地址嗎?
- 這 12 個助記詞是如何變成你的私鑰和地址的?
恭喜你來對地方了,本文將帶你一步步使用 Rust 語言將助記詞轉化為地址。了解助記詞到地址之間的過程,不僅能加深對區塊鏈系統和加密原理的理解,還能提高就業機會,滿足相關開發需求,提高效率。因此,對於希望深入了解和從事區塊鏈技術開發的人來說,這些知識至關重要,不可或缺。
為什麼選擇 Rust#
Rust 是由 Mozilla 開發的一種通用編譯編程語言,因其可靠性和高效性越來越受到開發人員的青睞。因此,我相信未來以太坊生態系統中會有越來越多的項目使用 Rust 進行重構和升級。以太坊生態系統中還出現了許多基於 Rust 的知名項目,例如:
Foundry - 用於以太坊應用程序開發的快速、可移植和模塊化工具包。
Reth - 用於以太坊協議的模塊化、對貢獻者友好且速度極快的實現。
讓我們盡快了解以太坊中的 Rust!
不是開發者?#
沒關係!本文還提供了詳細的解釋和無代碼示例,幫助您了解整個過程。
步驟 1 - 生成助記詞#
助記詞是一串便於記憶和書寫的詞 / 字表。你以為助記詞是隨機生成的嗎?其實它並不是完全隨機的。讓我們深入看看。
助記詞中的每個詞都可以用 0 到 2047 之間的一個數字來表示,共 2048 個數字。您可以從 BIP39 詞表中獲取更多信息。另一個有趣的事情是,我們不僅可以使用英語助記詞,還可以使用簡體中文、繁體中文、日語、韓語和西班牙語助記詞。例如:
英語: indoor dish desk flag debris potato excuse depart ticket judge file exit
韓語: 수집 몸속 명의 분야 만족 인격 법원 멀리 터미널 시멘트 부작용 변명
中文簡體: 诗 失 圆 块 亲 幼 杂 却 厉 齐 顶 互
中文繁體: 詩 失 圓 塊 親 幼 雜 卻 厲 齊 頂 互
回看問題 1:可以從詞典中創建自己的助記詞嗎?
不能。可以用作助記詞的單詞 / 文字數量有限,且最後一個詞是 "校驗碼",即:最後一個詞是前面所有詞的計算結果,和前面十個詞有對應關係。實際上,我們身份證號的最後一位也是 “校驗碼”,助記詞也與之類似。
接下來我們可以把助記詞轉換成二進制數,比如 "indoor" 是 920,二進制數就是 1110011000。每個二進制數都是 11 位。
助記詞(Mnemonic)
indoor dish desk flag debris potato excuse depart ticket judge file exit
二進制下的助記詞(MnemonicBinary)
01110011000 00111111001 00111011111 01011000001 00111000011 10101000110 01001111000 00111010110 11100001101 01111000101 01010110001 01001111111
二進制下的熵(Entropy)
01110011000 00111111001 00111011111 01011000001 00111000011 10101000110 01001111000 00111010110 11100001101 01111000101 01010110001 0100111
十六進制熵(EntropyInHex)
7307e4efac13875193c1d6e1af1558a7
二進制下的校驗碼(CheckSum)
1111
熵的二進制長度(lengthOfEntropy)
128
助記詞的二進制長度(lengthOfMnemonicBinary)
132
助記詞 = 熵 + 校驗碼
校驗碼 = SHA256(entropy)[0:len(entropy)/32]=SHA256(entropy)[0:4]=
換句話說,我們可以隨機生成一串 01 組合作為助記詞的材料,也就是熵。熵的長度是 32 的倍數。在 12 個助記詞的情況下,熵的長度為 128。
這也意味著助記詞的長度是 3 的倍數,但最多不超過 24 個詞。詞數越多越安全。
為了確保助記詞有效,我們需要計算助記詞的校驗和。校驗和是熵的 SHA256 哈希值的前幾位。接下來我們就得到二進制長度為 132 位,可以將其轉換為 12 個助記詞(11 位一個詞)。
fn step1_generate_mnemonic() {
// generate mnemonic
let entropy = &[
0x33, 0xE4, 0x6B, 0xB1, 0x3A, 0x74, 0x6E, 0xA4, 0x1C, 0xDD, 0xE4, 0x5C, 0x90, 0x84, 0x6A,
0x79,
];
let mnemonic = Mnemonic::from_entropy(entropy, Language::English).unwrap();
// let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English);
// let mnemonic: Mnemonic = Mnemonic::from_phrase(
// "indoor dish desk flag debris potato excuse depart ticket judge file exit", // It will be unvalid, if you change any word in it.
// Language::English,
// )
// .unwrap();
let phrase = mnemonic.phrase();
println!("Generated Mnemonic: {}", phrase);
}
步驟 2 - 助記詞到種子#
要將助記詞轉換為種子,我們需要使用 PBKDF2 函數,將助記詞作為密碼,將字符串 "助記詞" 和口令作為鹽值。
PBKDF2 的主要功能是將密碼轉換成加密密鑰。與傳統的單次散列函數不同,PBKDF2 通過將密碼與鹽值結合並多次重複應用散列函數來生成密鑰。
通常情況下,迭代次數設置為 2048 次,並使用 HMAC-SHA512 作為散列函數,提高暴力破解的難度。SHA512 可以理解為生成 512 位哈希值的函數,也因此種子長度為 512 位(64 字節)。
fn step2_mnemonic_to_seed(mnemonic: &Mnemonic) -> String {
let seed = Seed::new(mnemonic, "");
seed.as_bytes();
hex::encode(seed.as_bytes())
}
// return 3bd0bda567d4ea90f01e92d1921aacc5046128fd0e9bee96d070e1d606cb79225ee3e488bf6c898a857b5f980070d4d4ce9adf07d73458a271846ef3a8415320
第 3 步 - 將種子轉入主密鑰#
BIP32
現在,我們需要深入了解 BIP32 - Hierarchical Deterministic Wallets(分層確定性錢包)。HDW 的主要目的是更好地管理錢包。只有一個種子可以恢復由它生成的所有錢包。有了 HDW,我們每次進行交易時都可以使用一個新地址,從而更好地確保匿名性。雖然 BIP32 最初是為比特幣設計的,但其原理可以應用於其他加密貨幣,允許同一種子為多種貨幣生成多個錢包地址。所有的層次結構都來自主密鑰,現在就讓我們仔細看看。
fn step3_seed_to_master_key(seed_hex: &String) -> (String, String) {
let seed_bytes = hex::decode(seed_hex).unwrap();
let key = hmac::Key::new(hmac::HMAC_SHA512, b"Bitcoin seed");
let tag = hmac::sign(&key, &seed_bytes);
let (il, ir) = tag.as_ref().split_at(32);
(hex::encode(il), hex::encode(ir))
}
// return 5e01502044f205b98ba493971561284565e41f34f03494bb521654b0c35cb3a9 bccd1f17319e02baa4b2688f5656267d2eeaf8b49a49607e4b37efe815629c82
通過主密鑰,我們可以用不同的派生路徑派生出子密鑰。
回看問題 2:為什麼助記詞很重要?為什麼我不能把我的助記詞送給別人?
助記詞是找回你所有錢包的關鍵。如果你丟失了私鑰,您將無法訪問您的資產。如果您洩露了助記詞,由該助記詞衍生的賬戶中的所有資產都可能被惡意者竊取。
步驟 4 - 主密鑰到私鑰#
有了主密鑰和鏈碼,我們就可以為第一個賬戶導出私鑰了。
讓我們先檢查一下派生路徑:
m / purpose' / coin_type' / account' / change / address_index
- m 是主密鑰;
- purpose 為 44',表示 BIP44;coin type 為 0',表示比特幣;60',表示以太坊。
- account是賬戶的索引,從 0 開始。您可以將 0 定義為日常使用的主賬戶,將 1 定義為捐贈或其他用途的賬戶。
- change 字段用於區分內部鏈和外部鏈。
- address 是鏈上地址的索引。你可以用它來生成多個地址。
要獲取私鑰,我們需要從主密鑰中派生私鑰和每一級的路徑參數。
派生函數的作用如下:
// CKDpriv((key_parent, chain_code_parent), i) -> (child_key_i, child_chain_code_i)
// `i` is the level number.
// CKDpriv: child key derivation (private)
pub fn derive_with_path(
master_private_key: SecretKey,
master_chain_code: [u8; 32],
path_numbers: &[u32; 5],
) -> SecretKey {
let mut depth = 0;
let mut child_number: Option<u32> = None;
let mut private_key = master_private_key;
let mut chain_code = master_chain_code;
for &i in path_numbers {
depth += 1;
println!("depth: {}", depth);
child_number = Some(i);
println!("child_number: {:?}", child_number);
(private_key, chain_code) = derive(child_number.unwrap(), private_key, chain_code);
}
private_key
}
pub fn derive(
child_number: u32,
private_key: SecretKey,
chain_code: [u8; 32],
) -> (SecretKey, [u8; 32]) {
println!("child_number: {:?}", child_number);
let child_fingerprint = fingerprint_from_private_key(private_key.clone());
println!("child_fingerprint: {:?}", hex::encode(child_fingerprint));
let derived = derive_ext_private_key(private_key.clone(), &chain_code, child_number);
let private_key = derived.0;
let chain_code = derived.1;
println!("private_key: {:?}", hex::encode(private_key.as_ref()));
println!("chain_code: {:?}\n", hex::encode(chain_code));
(private_key, chain_code)
}
// Calculate the fingerprint for a private key.
pub fn fingerprint_from_private_key(k: SecretKey) -> [u8; 4] {
let pk = curve_point_from_int(k);
// Serialize the public key in compressed format
let pk_compressed = serialize_curve_point(pk);
// Perform SHA256 hashing
let sha256_result = digest::digest(&digest::SHA256, &pk_compressed);
// Perform RIPEMD160 hashing
let ripemd_result = ripemd160::Hash::hash(sha256_result.as_ref());
// Return the first 4 bytes as the fingerprint
ripemd_result[0..4].try_into().unwrap()
}
// Derived ExtPrivate key
pub fn derive_ext_private_key(
private_key: SecretKey,
chain_code: &[u8],
child_number: u32,
) -> (SecretKey, [u8; 32]) {
let key = hmac::Key::new(hmac::HMAC_SHA512, chain_code);
let mut data = if child_number >= (1 << 31) {
[&[0u8], &private_key[..]].concat()
} else {
let p = curve_point_from_int(private_key);
serialize_curve_point(p)
// private_key.as_ref().to_vec()
};
data.extend_from_slice(&child_number.to_be_bytes());
let hmac_result = hmac::sign(&key, &data);
let (l, r) = hmac_result.as_ref().split_at(32);
let l = (*l).to_owned();
let r = (*r).to_owned();
let mut l_32 = [0u8; 32];
l_32.clone_from_slice(&l);
let private_byte = private_key.as_ref();
let l_secret = SecretKey::from_slice(&l).unwrap();
let child_private_key = l_secret
.add_tweak(&Scalar::from_be_bytes(*private_byte).unwrap())
.unwrap();
let child_chain_code = r;
(child_private_key, child_chain_code.try_into().unwrap())
}
derive_with_path 函數以派生路徑(path_numbers)、主密鑰(master_master_key)、主鏈碼(master_chain_code)迭代調用派生函數(derive),計算出對應賬戶的私鑰。比如,我們可以通過路徑 "m/44'/ 60'/ 0'/ 0/ 0" 獲取第一個賬戶的私鑰,通過路徑 "m/44'/ 60'/ 0'/ 0/ 1" 獲取第二個賬戶的私鑰。
回看問題 3:我可以用我的 ETH 私鑰生成我的 BTC 地址嗎?
不能。比特幣的派生路徑是 m/44'/0'/0'/0,ETH 的派生路徑是 m/44'/60'/0'/0/0。我們不能用子私鑰計算父私鑰,這也意味著,如果我們有了主密鑰,我們就可以計算所有鏈上的所有錢包。
還有一點需要說明的是,路徑中的撇號表示使用了 BIP32 強化派生,如 44' 是強化派生,而 44 不是。而 44' 的實際意思是 2³¹+44。
BIP32 中的 "強化" 可提高派生密鑰的安全性,使其無法僅使用一個公開密鑰和一個子密鑰來派生其他子密鑰,從而有效防止潛在攻擊者訪問你的密鑰層次結構。
fn step3_master_kay_to_private_key(
master_secret_key_hex: String,
master_chain_code_hex: String,
derived_path: [u32; 5],
) -> String {
let master_secret_key_vec = hex::decode(master_secret_key_hex).unwrap();
let master_secret_key: &[u8] = master_secret_key_vec.as_ref();
let master_code_vec: Vec<u8> = hex::decode(master_chain_code_hex).unwrap();
let master_code: &[u8] = master_code_vec.as_ref();
let private_key = derive_with_path(
SecretKey::from_slice(master_secret_key.clone()).unwrap(),
master_code.try_into().unwrap(),
&derived_path,
);
hex::encode(private_key.as_ref())
}
步驟 5:私鑰到公鑰#
困難的部分已經結束了,現在我們可以從私鑰中獲取公鑰啦!
公鑰是椭圓曲線上的一個點,通過將私鑰與生成點相乘來生成。
fn step5_private_key_to_public_key(private_key_hex: String) -> String {
let private_key_vec = hex::decode(private_key_hex).unwrap();
let private_key = SecretKey::from_slice(private_key_vec.as_ref()).unwrap();
let public_key = curve_point_from_int(private_key);
hex::encode(serialize_curve_point(public_key))
}
步驟 6 - 公鑰地址#
地址是公鑰 Keccak-256 哈希值的最後 20 個字節。
fn step6_public_key_to_address(pub_key_hex: String) -> String {
let public_key_vec = hex::decode(pub_key_hex).unwrap();
let public_key = PublicKey::from_slice(public_key_vec.as_ref()).unwrap();
let serialized_pub_key = public_key.serialize_uncompressed();
let public_key_bytes = &serialized_pub_key[1..];
let mut hasher = Keccak::v256();
hasher.update(public_key_bytes);
let mut output = [0u8; 32];
hasher.finalize(&mut output);
let address = &output[12..];
hex::encode(address)
}
回望問題 4:這 12 個助記詞是如何變成你的私鑰和地址的?
總之,助記詞由熵生成,然後通過 PBKDF2 轉換成種子。種子用於通過 HMAC-SHA512 生成主密鑰和鏈碼。通過特定路徑,從主密鑰中提取私鑰。最後,私鑰生成公鑰,公鑰的 Keccak-256 哈希值的最後 20 個字節構成以太坊地址。
參考資料#
推薦工具#
BIP39 - Mnemonic Codebitcoin mnemonic converteriancoleman.io