HD Wallets的全稱是Hierachical Deterministic Wallets, 對應中文是 分層確定性錢包。
這種錢包能夠使用一組助記詞來管理所有的賬戶的所有幣種,在比特幣的BIP32提案中提出,通過種子來生成主私鑰,然后派生海量的子私鑰和地址。種子很長,為了方便記錄,轉換為一組單詞記錄,這是BIP39提出的。
生成錢包地址的基本流程:1 生成一組助記詞 2 助記詞轉化成種子(通過PBKDF2) 3 種子生成根私鑰(通過HMAC-SHA512) 4 通過根私鑰生成子私鑰
本文的目的是帶着讀者用代碼實現一個HD錢包開發。
開發過程中要用到兩個第三方庫,一個是Hooked-Web3-Provider,一個是LightWallet。
Hooked-Web3-Provider使用HTTP與geth通信,可以使用秘鑰來簽署調用交易sendTransaction的實例,因此不需要創建交易數據部分。直接調用sendTransaction完成生成交易數據,發送交易,廣播給全網。
LightWallet是一個實現BIP32、BIP39和BIP44的HD錢包。
LightWallet提供API來創建和簽署交易,或者使用LightWallet生成的地址和密鑰加密和解密數據。它的主要目的是為Hooked-Web3-Provider提供一個簽名提供方。它的命名空間有四個,即keystore、signing、encryption和txutils。
signing、encryption和txutils三個命名空間分別用來簽名,非對稱加密,生成交易,它們的名字大概能反應出各自的功能。keystore命名空間用來生成種子,keystor,這是一個存儲加密種子和秘鑰的對象。keystore對於其中發現的地址可以自動簽名。
HD 錢包中的密鑰是用"路徑"命名的,且每個級別之間用斜杠(/)字符來表示。由主私鑰衍生出的私鑰起始以"m"打頭。因此,第一個母密鑰生成的子私鑰是 m/0。第一個公共鑰匙是 M/0。第一個子密鑰的子密鑰就是 m/0/1,以此類推。
密鑰的"祖先"是從右向左讀,直到你達到了衍生出的它的主密鑰。舉個例 子,標識符 m/x/y/z 描述的是子密鑰 m/x/y 的第 z 個子密鑰。而子密鑰 m/x/y 又是 m/x 的第 y 個子密鑰。m/x 又是 m 的第 x 個子密鑰。
代碼實現
1. 啟動geth網絡
假設已經安裝好geth。使用命令啟動geth網絡:
geth --networkid 15 --dev --dev.period 1 --rpc --rpcapi "db,eth,net,web3,miner,personal" --rpccorsdomain "*" --rpcaddr "0.0.0.0" --rpcport "8545" console 2>>log
這個命令指定了networkid是15,當然這是隨便取的。 --dev --dev.period 1 --dev是開發網絡,但是geth后面的版本中為了方便開發,如果不加--dev.period 1 不會自動挖礦。 --rpcaddr "0.0.0.0" 這樣設置是為了讓所有的網絡都能連上geth。 指定端口8545. console表明啟動玩登錄到控制台。

2.構建前端
項目代碼結構(參考《區塊鏈項目開發指南》):

app.js內容如下:
var express = require("express"); var app = express(); app.use(express.static("public")); app.get("/", function(req, res){ res.sendFile(__dirname + "/public/html/index.html"); }) app.listen(8080);
構建前端的一個node服務。
index.html 頁面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<link rel="stylesheet" href="/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
<br>
<div class="alert alert-info" id="info" role="alert">
Create or use your existing wallet.
</div>
<form>
<div class="form-group">
<label for="seed">Enter 12-word seed</label>
<input type="text" class="form-control" id="seed">
</div>
<button type="button" class="btn btn-primary" onclick="generate_addresses()">Generate Details</button>
<button type="button" class="btn btn-primary" onclick="generate_seed()">Generate New Seed</button>
</form>
<hr>
<h2 class="text-xs-center">Address, Keys and Balances of the seed</h2>
<ol id="list">
</ol>
<hr>
<h2 class="text-xs-center">Send ether</h2>
<form>
<div class="form-group">
<label for="address1">From address</label>
<input type="text" class="form-control" id="address1">
</div>
<div class="form-group">
<label for="address2">To address</label>
<input type="text" class="form-control" id="address2">
</div>
<div class="form-group">
<label for="ether">Ether</label>
<input type="text" class="form-control" id="ether">
</div>
<button type="button" class="btn btn-primary" onclick="send_ether()">Send Ether</button>
</form>
</div>
</div>
</div>
<script src="/js/web3.min.js"></script>
<script src="/js/hooked-web3-provider.min.js"></script>
<script src="/js/lightwallet.min.js"></script>
<script src="/js/main.js"></script>
</body>
</html>
重點在main.js
generate_seed函數:
function generate_seed() { var new_seed = lightwallet.keystore.generateRandomSeed(); //生成一個隨機的種子 document.getElementById("seed").value = new_seed; //放到頁面 generate_addresses(new_seed); } var totalAddresses = 0; function generate_addresses(seed) { if(seed == undefined) { seed = document.getElementById("seed").value; } if(!lightwallet.keystore.isSeedValid(seed)) //判斷種子是否是有效的種子 { document.getElementById("info").innerHTML = "Please enter a valid seed"; return; } totalAddresses = prompt("How many addresses do you want to generate"); //用戶輸入想生成的賬戶的個數 if(!Number.isInteger(parseInt(totalAddresses))) //確保輸入是一個數字 { document.getElementById("info").innerHTML = "Please enter valid number of addresses"; return; } var password = Math.random().toString(); //隨機生成一個密碼 這個密碼可以由用戶輸入,也可以自動生成。這里為了方便,提升體驗,自動生成一個。 lightwallet.keystore.createVault({
// 使用createVault方法創建keystore實例。createVault用一個對象和
// 一個回調函數作為參數。對象可以有4種屬性:password、seedPharse、
// salt和hdPathString。password是必選項,其他的都是可選項。如果不提
// 供seedPharse,它會生成和使用一個隨機seed。拼接salt與password,以
// 提高對稱密鑰加密技術的安全性,因為攻擊者不僅要找到password還得
// 找到salt。如果不提供salt,它就會隨機生成。keystore命名空間存儲未加
// 密的salt。hdPathString用於為keystore命名空間提供默認衍生路徑,即生
// 成地址、簽署交易等。如果不提供衍生路徑,則使用該衍生路徑。如果
// 不提供hdPathString,則默認值為m/0'/0'/0'。這個衍生路徑的默認目的是
// 簽名。可以創建新的衍生路徑或者使用keystore實例的
// addHdDerivationPath()方法重寫當前衍生路徑的purpose。還可以使用
// keystore實例的setDefaultHdDerivationPath()方法改變默認衍生路徑。
// 最后,一旦keystore命名空間被創建,就通過回調函數返回實例。所
// 以,這里僅用keyword和seed創建了一個keystore。
password: password, seedPhrase: seed }, function (err, ks) { ks.keyFromPassword(password, function (err, pwDerivedKey) { //使用這個方法生成對應數量的地址和秘鑰 if(err) { document.getElementById("info").innerHTML = err; } else { ks.generateNewAddress(pwDerivedKey, totalAddresses); var addresses = ks.getAddresses(); var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); //創建web3和本地的連接,如果是遠程把localhost改成對應的ip即可。 var html = ""; for(var count = 0; count < addresses.length; count++) { var address = addresses[count]; var private_key = ks.exportPrivateKey(address, pwDerivedKey); //使用該方法解碼和檢索地址私鑰。 var balance = web3.eth.getBalance("0x" + address); html = html + "<li>"; html = html + "<p><b>Address: </b>0x" + address + "</p>"; html = html + "<p><b>Private Key: </b>0x" + private_key + "</p>"; html = html + "<p><b>Balance: </b>" + web3.fromWei(balance, "ether") + " ether</p>"; html = html + "</li>"; } document.getElementById("list").innerHTML = html; } }); }); }
發送eth:
function send_ether() { var seed = document.getElementById("seed").value; if(!lightwallet.keystore.isSeedValid(seed)) { document.getElementById("info").innerHTML = "Please enter a valid seed"; return; } var password = Math.random().toString(); lightwallet.keystore.createVault({ password: password, seedPhrase: seed }, function (err, ks) { ks.keyFromPassword(password, function (err, pwDerivedKey) { if(err) { document.getElementById("info").innerHTML = err; } else { ks.generateNewAddress(pwDerivedKey, totalAddresses); ks.passwordProvider = function (callback) { callback(null, password); }; var provider = new HookedWeb3Provider({ host: "http://localhost:8545", transaction_signer: ks }); //利用ks做交易的簽署者。它調用ks的hasAddress方法和signTransactions方法
var web3 = new Web3(provider); var from = document.getElementById("address1").value; var to = document.getElementById("address2").value; var value = web3.toWei(document.getElementById("ether").value, "ether"); web3.eth.sendTransaction({ from: from, to: to, value: value, gas: 21000 }, function(error, result){ if(error) { document.getElementById("info").innerHTML = error; } else { document.getElementById("info").innerHTML = "Txn hash: " + result; } }) } }); }); }
測試:
到項目根目錄,執行 npm install 加載相關庫。
執行 node app.js 啟動前端服務。端口8080. 訪問 localhost:8080

點擊 generate new seed ,輸入2,生成兩個賬戶。

然后進入到一開始打開的geth客戶端,通過 eth.accounts 查看geth下的賬號。復制地址,通過eth.getBalance("XX") 來獲取地址余額。

轉入一部分余額到上面生成的地址中,例如0xb4c7cf322956f0345b613f246d5d2f4ba03028f6.
eth.sendTransaction({from: "0x8d1c1dd6f48c33c97924e5f310905e1822a6cbd0", to: "0xb4c7cf322956f0345b613f246d5d2f4ba03028f6", value: web3.toWei(100, "ether")})
點擊頁面的 generate details 刷新,同樣的seed會生成同樣的賬戶。

這個時候會發現賬戶多出100eth。然后可以測試錢包內地址,錢包對錢包外地址轉賬。
項目giithub地址:https://github.com/figo050518/wallet
