HD Wallet

徐粲邦
12 min readNov 17, 2016

--

前言

Deterministic wallet 的概念源自於,我能不能產生很多 key 但卻只用一個 seed 來產生就好?而最有名的這種 wallet 就是 BIP32 所提出的 Hierarchical Deterministic wallet (HD wallet) 了,他的概念是用一個 seed 產生一個 key,也叫做 master key,然後這個 key 又可以繼續往下一層產生更多 keys,而第二層的 keys 也都可以再往下產生多個 keys,值得注意一下的是上面提到的 key 可以是 private key 或是 public key,而 HD wallet 的架構如下圖所示

那這樣的一個錢包有什麼用處呢?他的第一個好處是他有樹的架構,不同公司的部門可以在同一層擁有不同的 key,彼此不會干擾對方,這種樹狀結構再有組織架構的系統下可以有很好的發揮。第二個優點是 HD wallet 安全性,HD wallet 可以不用產生 private key,就可以得到許多 public key / address,詳細介紹下面 產生 child public key 那節會講。第三個好處同時也是最大的風險是移植性,當樹根的 seed 搬到一個新的環境或資料庫時,只要每個使用者記得自己是第幾層、編號多少,就可以很快速地得到所有使用者的 key 跟 address 了,但同時風險也代表著,如果 seed 流出去了,他底下的所有 key 跟 address 都暴露在危險之中了。

再進到接下來的簡介之前,你必須要先知道 private key / public key / address 他們三個的關係,還有他們分別可以幹嘛的,然後我會邊用一個 python 的 library 叫做 bip32utils 來示範。

產生 master key

第一個步驟當然是用 seed 產生最一開始的 master key,而 seed 的長度這邊有規定要是 128、256 或是 512 bits 才行,雖然我們用的那個 python 套件其實只要用不小於 128 bits 的 seed 就會成功了,然後將你的 seed 丟去 HMAC-SHA256 演算法之後會產生一個長度為 512 bits 的結果,而前 256 bits 就為你的 master private key,後 256 bits 則是一個叫做 master chain code 的東西,他基本上代表一個 entropy 是當你再往下產生 child keys 時才會用到的。流程如下圖

產生 child private key

HD wallet 在往下一層產生 key 的時候用一個 function 來描述這件事,這個 function 叫做 child key derivation (CKD),他其實跟剛剛一樣其實也只是做一次 HMAC-SHA256 演算法而已,只是這個 function 定義要吃三個參數。

  1. parent public key,剛剛我們已經得到了 private key,而用這個 private key 產生的 public key 就是了
  2. parent chain code,這就是剛剛提到的 chain code,他其實就是代表一個亂度而已
  3. index,是你要產生下一層的第幾個 key 的意思,例如 0 則代表產生出來的 key 是你的第一個小孩

出來的結果因為演算法一樣,所以還是一個長度為 512 bits 東西,可以猜想的他一樣可以分成前 256 bits 是 child private key,後 256 bits 為 child chain code,然後就可以在往下一層產生更多小孩了!流程如下圖

這邊有幾個議題可以探討一下,首先因為 HMAC-SHA256 演算法是不可逆的,所以不能拿 child key 去推回去 parent key,再來同一層的 child key 是不會知道他的兄弟姐妹的,因為 child key 並不知道 parent chain code,而他也不能從自己的 chain code 推回去,所以每個 child key 是彼此獨立的,除非你是 parent key 才能知道每個你底下的 child key。

現在我們可以用 bip32utils 來玩玩看了!

>>> from bip32utils import BIP32Key
>>> root = BIP32Key.fromEntropy('0000000000000000')
>>> master_key = hexlify(root.PrivateKey())
>>> master_key
'b3a66de77358af686e9ce1587cb6f48d3e156e660b9910bd65c051f6b8bfe366'
>>> child_0 = root.ChildKey(0)
>>> hexlify(child_0.PrivateKey())
'1c7b4da854acd6696aace5c0bf2fd9325c0eab0ecb998d2ff5db696ae017f01d'
>>> child_123 = root.ChildKey(123)
>>> hexlify(child_123.PrivateKey())
'bdb35402257ad3dd46e4b1f525d291c4edbd2965c8a3b3e3ec15633aa9750dbf'
>>> grandchild_1000_99 = root.ChildKey(1000).ChildKey(99)
>>> hexlify(grandchild_1000_99.PrivateKey())
'ad3aedad40351d93e7658abd04bbb51c5950c72de4665d18c99443427b4ecd3f'

再介紹上面這段範例之前我們先來定義一下 path,這是用來表達某個 node 在這棵樹裡面的路徑,例如 m/0 就代表是 master private key 的第 0 個 child private key,這邊我們可以看到我們用 0000000000000000 當作 seed 來產生 root,然後又分別產生了 m/0、m/123 和 m/1000/99。是不是很簡單!

Extended Key

上面的 CKD 可以知道其實產生 child 過程中最重要的兩個參數就是 parent private key 和 parent chain code 了,想像如果現在有一個長長的字串可以代表這個 key,也就是說你可以用這個長長的字串來繼續產生 child key,而 extended key 實際上是不只由上面兩個參數所組成的

  1. 4 bytes version,這邊其實只有四種可能,0x0488B21E 代表 mainnet public,0x0488ADE4 代表 mainnet private,0x043587CF 代表 testnet public,0x04358394 代表 testnet private
  2. 1 byte depth,就是當前這個 node 在樹的深度
  3. 4 bytes fingerprint,fingerprint 是 identifier 的前 32 bits,而 identifier 是 把 public key 做一次 hash160 的結果,可以用來代表這個 node
  4. 4 bytes child number,即他是第幾個 child
  5. 32 bytes chain code,為我們剛剛提到的兩個重要參數的其中之一
  6. 33 bytes private key 或 public key,因為 private key 長度只有 32 bytes,所以前綴要補 0x00

以上總共 78 bytes 串接在一起,然後再做 base58 encoding。現在我們再來試試看 extended key 可以怎麼用

>>> from bip32utils import BIP32Key
>>> root = BIP32Key.fromEntropy('0000000000000000')
>>> node = root.ChildKey(123).ChildKey(456).ChildKey(789)
>>> hexlify(node.PrivateKey())
'5e74be729897520619930aaf5b2be8de94c554d95a99ae9e78b8a3a8f8e8a3af'
>>> extended_key = root.ChildKey(123).ChildKey(456).ChildKey(789).ExtendedKey()
>>> node = BIP32Key.fromExtendedKey(extended_key)
>>> hexlify(node.PrivateKey())
'5e74be729897520619930aaf5b2be8de94c554d95a99ae9e78b8a3a8f8e8a3af'

可以看出來結果是一樣的嗎?你可以試試看如果 node 很深的時候,如果本來有記得 extended key 的話,會明顯快很多!因為他只需要把 extended key 解開就可以得到這個 key 了,不需要才從 root 去慢慢產生。

產生 child public key

還記得前面提過 HD wallet 有可以透過 parent public key 產生很多 child public key 而不需要透過 private key 的特性嗎?這樣做的好處是安全性,在冷錢包情形下,我們可以在用一樣的 seed 得到 private key 且存在離線裝置上,但 public key 還是可以放在網路上,因為他是透過 parent public key 產生的,看不到 private key 所以可以不用擔心 address 裡的錢會被盜用,而當我們需要花費那筆錢 的時候,就可以離線的做 transaction 簽章,再 broadcast 到網路上。而產生 child public key 的流程如下

可以發現其實根本來的 CKD 很像,這也是為什麼我們剛剛說 CKD 其實不一定是指 private key,也可以是 public key 的原因,最後當然我們也可以用 bip32utils 來測驗一下

>>> root = BIP32Key.fromEntropy('0000000000000000', public=True)
>>> node = root.ChildKey(123).ChildKey(456).ChildKey(789)
>>> hexlify(node.PublicKey())
'02660afb11d43e91a782d5f15515b933e1b1c0b2b0f2491ebacbaa67a7a8cb756d'
>>> root = BIP32Key.fromEntropy('0000000000000000')
>>> node = root.ChildKey(123).ChildKey(456).ChildKey(789)
>>> hexlify(node.PublicKey())
'02660afb11d43e91a782d5f15515b933e1b1c0b2b0f2491ebacbaa67a7a8cb756d'

上面三行的有 public=True 是代表我們採用 child public key derivation,而下面三行就是原本的 child private key derivation,結果根本一樣!差別只是當在上面的情況下你嘗試去拿 node 的 private key 時他會跟你說找不到。

Hardened CKD

還記得 extended key 嗎?在上一節裡我們一樣可以用 extended key 來做操作,而 extended public key 裡面含有 chain code,如果不巧今天有一個 child private key 不小心洩漏出去了,則 parent chain code 搭配這個 child private key 就可以產生所有其他的 child private key 了,更糟糕的是 parent private key 也會被知道,簡單來說就變成一開始的那種模式了,就沒有剛剛我們說的安全性的優點了。

為了處理這種情形,就需要用到 hardened CKD 了,他用 parent private key 來取代 parent public key 產生 child chain code,過程如下圖

而 index 的長度為 32 bits,所以前一半的量可以來當作普通的 CKD 用途,而後一半就保留給了 hardened CKD 了。

以上就是 HD wallet 的簡介,其實不太看重細節的話他還算蠻簡單易懂的,然後我覺得搭配 bip32utils 的原始碼一起看,也會幫助自己更了解他怎麼做的。HD wallet 實際上可以運用的場景也蠻多的,像這次因為一個案子是某個銀行下面有許多點數平台,而每個點數平台又有自己的使用者,然後要做交易清算,這種場景就可以套用一個三層的 HD wallet 來實作。

--

--