はじめに
次世代ビジネス推進部FY21新卒の荻原です。
今回は「HDウォレット」について紹介します。先日、暗号資産の財布?〜ウォレットについて〜では暗号資産のウォレットについて、ブロックチェーンに接続する際に使用する秘密鍵というデータを保管するためのものだという紹介をしました。今回はその秘密鍵をどのように生成するかという部分に焦点を当てたお話です。
NFTを発行したり保有したりするためにも秘密鍵が必要不可欠となるので、自分自身NFTに関して勉強や開発をする中でHDウォレットに関する知識は特に重要だと感じています。
目次
1. 秘密鍵とアドレスについて
暗号資産の財布?〜ウォレットについて〜でも説明した秘密鍵とアドレスについて、復習と補足の内容です。
自分が保有している暗号資産の情報は分散型ネットワーク上のブロックチェーンに記録されています。そのため、取引をする際はそのネットワークに接続する必要があります。
接続には秘密鍵というキーが必要になり、それがパスワードのような役割をしてそのアカウントが保有する暗号資産の証明になります。
また、取引の際にはアドレスという文字列も用います。これは送金相手を指定するための、銀行の口座番号のようなものです。
秘密鍵とアドレスは次のような流れで生成されます。
シードは乱数です。秘密鍵、公開鍵、アドレスはシードを基にHash関数によって作られた文字列のデータです。Hash関数とは、ある値を別の値に不可逆的に変換する関数です。不可逆なので、アドレスから公開鍵、公開鍵から秘密鍵、秘密鍵からシードを求めることは実質不可能です。
このように、公開してアカウントの指定に用いるアドレスと、本人しか知り得ない秘密鍵を生成します。
アカウントはリスク分散や用途によって使い分けるために複数作成することがあります。今回はどのように秘密鍵やアドレスをたくさん作るかについて説明します。
2. 秘密鍵の生成方法
秘密鍵の生成方法には2種類あります。
1つ目のランダムウォレットはその名の通りランダムに生成された複数のシードからそれぞれ秘密鍵を生成します。仕組みは単純ですが、保有するアカウントの数だけ秘密鍵を保管しておかなければいけません。
2つ目のHDウォレットが今回の本題です。HDウォレットは「Hierarchical Deterministic ウォレット」の略で、日本語で「階層的決定性ウォレット」です。一言で言うと、シードから生成された1つのマスターキーから階層(木)構造を取ってたくさんの秘密鍵を決定(生成)していきます。つまり、保管する必要があるのは大元の1つのシードのみです。
HDウォレットは「BIP32」というビットコインに関するルールで定められています。
3. HDウォレットの仕組み
HDウォレットの仕組みをもう少し詳しく説明していきます。
まずは、シードからマスターキーとなる秘密鍵を生成する方法です。
シードをHMAC-SHA512というHash関数に通し、得た文字列を半分に分けます。そしてその前半をマスターキーとなる秘密鍵、後半をチェーンコードとします。チェーンコードはマスターキーの子となる秘密鍵を生成するために使用します。
では、シードから次々に秘密鍵を生成していく流れを詳しく見ていきます。
シードから親秘密鍵、親チェーンコードを得るところまでは上記で説明した流れです。そしてそれらに0,1,2,3…というインデックスを合わせてHash関数(HMAC-SHA512)に通すことで子秘密鍵、子チェーンコードが得られます。インデックスを用いることによって子秘密鍵を複数作ることができるのです。
そしてさらに子秘密鍵と子チェーンコード、インデックスをHash関数に通すことで孫秘密鍵、孫チェーンコードが得られます。
では、これらの計算がどのように行われているのかを見ていきます。
まず前提として、楕円曲線暗号の一種である楕円曲線DSAを用いて、秘密鍵
kに対して公開鍵は楕円上の点
K=kG=(x, y)です。ここで、
Gとは楕円上の点
G=(x', y')です。
kGと
Gから
kを逆算することは実質不可能です。つまり、秘密鍵から公開鍵は求められますが、公開鍵を知っていても秘密鍵を求めることはできないということです。
図の左下から見ていきます。
親秘密鍵
k_pから親公開鍵
k_pGを得ています。そして親公開鍵、親チェーンコード、インデックスを組み合わせてHMAC-SHA512に通すことで得た文字列を半分に分け、
I_Rと
I_Lを得ます。
I_Rはチェーンコード
c_iです。
I_Lと親秘密鍵
k_pから子秘密鍵
k_p + I_L = k_iを得ます。子公開鍵は子秘密鍵から楕円上の点の計算か、親公開鍵と
I_Lからのどちらかで得ることができます。
親公開鍵のみ知っていれば子公開鍵を求めることができるということになりますが、これはHDウォレット特有だそうです。それによって秘密鍵を安全に保管したまま公開鍵を作成し、送金に必要なアドレスを次々に作成していくことができます。
4. 強化鍵
前章で親公開鍵から子公開鍵を計算できる流れがHDウォレット特有だという説明をしましたが、このことによってセキュリティ的に少し問題が出てきます。
HDウォレット秘密鍵の計算の図からわかるのですが、親公開鍵と親チェーンコードは公開されていているものなので、子秘密鍵を知っている人は I_Lも得ることができ、親秘密鍵を求めることができてしまいます。下の階層の秘密鍵から上の階層の秘密鍵の逆算や、そこから兄弟鍵の計算ができてしまうということです。
これを防ぐために強化鍵という仕組みがあります。これは親公開鍵から子公開鍵を求められない代わりに子秘密鍵から親秘密鍵の計算もできないようにしたものです。詳細の説明は今回は割愛しますが、強化鍵を用いたHDウォレットの全体像はこのようになります。
5. ニーモニック
シードは乱数なので人間が覚えるのは難しく、PCなどに保管しておかないと管理が難しいです。しかし、より安全に保管するにはオフラインで紙に書くなどの方法が良いとされています。
そこで、人間が管理しやすいようにシードは「ニーモニック」という12個や24個などの単語の羅列に変換されます。ニーモニックは「BIP39」で定義されています。
HDウォレットのハードウェアウォレットやウェブウォレットなどの初期設定の際も、ニーモニックを入力し、ウォレットにアカウントを復元します。MetaMaskでは「シークレットリカバリーフレーズ」と呼ばれています。
ニーモニックは日本語、英語、フランス語などの言語ごとに2048個のワードリストがあり、そこから組み合わせて作成されるそうです。
6. パスフレーズ
HDウォレットは1つのシードから複数のアカウントが作成できますが、アカウントを特定するにはシード以外にどの階層の何番目のアカウントかという情報も必要になります。
BIP32ではそのようにアドレスを特定するためのパスフレーズという形式があります。そしてこれを基に、種類の違う暗号資産でも1つのシードで管理できるようにしたものがBIP44で規定されています。
次の図はこちらからの引用ですが、パスフレーズの意味がわかりやすいと思います。
マスターキーからアカウントが複数作られ、それぞれ入金用とお釣り用に分かれ、そこからアドレスが多数生成されています。
7. PythonでHDウォレットを作成してみる
PythonのHDWallet(公式ドキュメント)というライブラリを使ってHD ウォレットを作成してみます。
まずはニーモニックを生成します。英語と日本語でそれぞれ作成しました。
from hdwallet.utils import generate_mnemonic mnemonic_e = generate_mnemonic(language='english') print(mnemonic_e) # roof inner boost outdoor hill ocean art unusual retire beauty type bicycle mnemonic_j = generate_mnemonic(language='japanese') print(mnemonic_j) # しょくたく たたかう ごかい かいてん みつける そむりえ このまま さます しかく たらす けいさつ ひまん |
文字列がニーモニックかどうかを判定する関数があったので、適当な英単語を12個並べてみたり上記で作成した正しいニーモニックの語順のみ変えてみたりしましたが、当然ですが Falseとなりました。
from hdwallet.utils import is_mnemonic # 正しく生成したニーモニック print(is_mnemonic('roof inner boost outdoor hill ocean art unusual retire beauty type bicycle')) # True # 適当な単語を並べた場合 print(is_mnemonic('cat dog rabbit elephant lion tiger pig bear cow horse human fish')) # False # ニーモニックの語順を変えた場合 print(is_mnemonic('roof inner bicycle boost outdoor ocean art unusual retire beauty type hill')) # False |
続いて、作成したニーモニックからウォレットを復元します。下記ではパスの指定がないため、マスターキーが求められている状態です。以下のコードの最後は、省略していますがニーモニックや秘密鍵など、アカウントの様々な情報を出力しています。
from hdwallet import HDWallet from hdwallet.symbols import ETH from pprint import pprint mnemonic = 'roof inner boost outdoor hill ocean art unusual retire beauty type bicycle' hd_wallet_account = HDWallet(symbol=ETH) \ .from_mnemonic(mnemonic=mnemonic) hd_wallet_account_info = hd_wallet_account.dumps() pprint(hd_wallet_account_info) # {'addresses': {'p2pkh': '0x84E0460aF12335c176211BcE264f0438501bA56f'}, # 'chain_code': '9f3ab29a7456a0b4edcfc51e1709be33290bcffc9218ae400dc75dba49ae7aad', # 'mnemonic': 'roof inner boost outdoor hill ocean art unusual retire beauty ' # 'path': None, # 'private_key': '8c26f5ed306d3ac188d8be51ed672e9843ab0a754c07bf15738867f1b423052d', # 'public_key': '02bd0140ad481a9d5eb6fadf6c1ba30d987067c56cecaab7c0f34b0c83bd1c3de3', # 'seed': 'bd20042d6a3104de9889104357871e3c9da20ddd636666975d79571d9bedcfbdf6dcf92e2a4057893c84b6fa538cc6f5fa90456d1aafb97921685a1d2245c16a', # } |
子以降の秘密鍵の復元の計算は複雑なのですが、親の部分に関して3. HDウォレットの仕組みの内容が正しいのかを確認してみます。
digestはシードをHMAC-SHA512に通して得たハッシュ値です。 digest_hexはその結果のハッシュ値を16進数で表したものです。これを前半と後半に分けたものが ilと irですが、それぞれ上記の hd_wallet_account_infoの private_keyと chain_codeと一致しています。
import hmac import hashlib seed = 'bd20042d6a3104de9889104357871e3c9da20ddd636666975d79571d9bedcfbdf6dcf92e2a4057893c84b6fa538cc6f5fa90456d1aafb97921685a1d2245c16a' # bytes型のハッシュ値 digest = hmac.new(b"Bitcoin seed", bytes.fromhex(seed), hashlib.sha512).digest() print(digest) # わかりやすいように16進数に変換 digest_hex = hmac.new(b"Bitcoin seed", bytes.fromhex(seed), hashlib.sha512).hexdigest() print(digest_hex) # 8c26f5ed306d3ac188d8be51ed672e9843ab0a754c07bf15738867f1b423052d9f3ab29a7456a0b4edcfc51e1709be33290bcffc9218ae400dc75dba49ae7aad # ハッシュ値を前半と後半に分ける il, ir = digest[:32], digest[32:] print(il.hex()) # 8c26f5ed306d3ac188d8be51ed672e9843ab0a754c07bf15738867f1b423052d print(ir.hex()) # 9f3ab29a7456a0b4edcfc51e1709be33290bcffc9218ae400dc75dba49ae7aad |
最後にパスを指定してMATICの1番目の入金用アカウントの1番目のアドレスの情報を求めてみます。ニーモニックやシードはマスターキーと変わらないことがわかります。
hd_wallet_account = HDWallet(symbol=ETH) \ .from_mnemonic(mnemonic=mnemonic) \ .from_path(path="m/44'/966'/0'/0/0") hd_wallet_account_info = hd_wallet_account.dumps() pprint(hd_wallet_account_info) # {'addresses': {'p2pkh': '0xE4Af83dF151cAFe805d445F87C6FaA774b65a099'}, # 'chain_code': '67543cb85d05ad3f6897a8dca50c936b5fb532883d1e1c848f8b7c157ed4cb0e', # 'hash': '936281aa5eeab455f198d601df4a9df30dcd775c', # 'mnemonic': 'roof inner boost outdoor hill ocean art unusual retire beauty ' # 'path': "m/44'/966'/0'/0/0", # 'private_key': '0de0f9a88fbbf4dce54366cc8a2e1f542a1f57ae15f67aa5705bd7f49e6d228b', # 'public_key': '02981cde8c64dc4864e475871e0d6d47a20e046609398063fe9f345f07a86918bb', # 'seed': 'bd20042d6a3104de9889104357871e3c9da20ddd636666975d79571d9bedcfbdf6dcf92e2a4057893c84b6fa538cc6f5fa90456d1aafb97921685a1d2245c16a', |
まとめ
- HDウォレットはシードという1つの乱数から無限にアドレスを作成する仕組みである。
- 特定のアドレスはシードとパスフレーズによって復元される。
- シードはニーモニックという単語の羅列で管理しやすいように表される。
最後まで読んでいただきありがとうございました。
参考
- 暗号通貨のHDウォレットの仕組み
- BIP39について調べてみました ワードリストからMnemonicを生成する仕組み
- HDウォレット(BIP-32)
- 【図解】HDウォレットとは? ウォレットアドレス生成の仕組みを解説
- 【ビットコイン】ウォレットの概要とHDウォレットの仕組み
- BIP44が分からなくなる話
- HDWallet v2.1.0 documentation
- GitHub python-hdwallet
- GitHub bips
この記事を書いた人
最近書いた記事
- 2023.12.24気難しいBlockchainへの対処法
- 2023.09.22レコチョクチケット特有の機能について
- 2023.07.18【ERC4337 その2】NFTを転送してみる
- 2023.07.14【ERC4337 その1】ERC4337について