BIP32を実装してみる

https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki

こんにちは、pjです。各種手続きが絶望的に苦手な自分には辛い年度末でした。皆さんは確定申告間に合いましたか…?

いきなりですが、今回から何回かは自分でウォレットを作成する時に必要な機能を実装していくことにします。初回は、ちょうど最近書く機会があったBIP32からやっていきます。今回は詳細な仕様には触れないので、各自BIP32や他の記事などを参照してください。

必要な機能

最低限必要だと思った昨日は次の通りです。

  1. Seedの作成
  2. マスター鍵の作成
  3. 子鍵の導出(拡張公開鍵、拡張秘密鍵)
  4. 拡張秘密鍵から拡張公開鍵の導出(Neuterと呼ばれる)
  5. 鍵のシリアライズ
  6. pathを指定して鍵を導出

実装してみた

ハッシュ関数、base58、secp256k1はライブラリを使いました。base58は簡単に実装できるので自分で実装してもよかったのかもしれません。key tree全体をBip32というクラスで、一つ一つの拡張鍵をExtkeyというクラスで管理しています。全体のコードはここにあります。

使い方

拡張秘密鍵を作る時は、

python bip32.py <path> <seed>

拡張公開鍵を作る時は、

python bip32.py <path> <seed> public

です。例えば

> python bip32.py m/0H/1/2H/2/1000000000 000102030405060708090a0b0c0d0e0f public
b'xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy'

解説

まず使うモジュールをimportします。sysはコマンドライン引数を入手するためだけに用いているので、その必要がない場合は要りません。

import hashlib
import hmac
import random
import base58
from secp256k1 import PrivateKey
import sys

次に、Key Tree全体を管理するBip32のコードです。

class Bip32:
def __init__(self, seed, network="mainnet"):
length = len(seed)
if length < 16 or 64 < length:
raise ValueError("specified seed size is not allowed")

self.seed = seed
self.network = network

@classmethod
def create_without_seed(network="mainnet"):
seed = random.getrandbits(256).to_bytes(int(32), "big")
return Bip32(seed, network)

def gen_masterpriv(self):
I64 = hmac.HMAC(key=b"Bitcoin seed", msg=self.seed,
digestmod=hashlib.sha512).digest()
return ExtKey(self.network, b"\x00", b"\x00\x00\x00\x00", b"\x00\x00\x00\x00", I64[32:], I64[:32], True)

def derive_from_path(self, path, is_private=True):
indexes = path.split("/")
for index in indexes:
is_hardened = False
if index == "m":
extkey = self.gen_masterpriv()
continue

if index[-1] == "H":
is_hardened = True
index = index[:-1]

childindex = int(index, 10)
if is_hardened == True:
childindex += 2147483648

extkey = extkey.derive_priv(childindex, is_hardened)

if not is_private:
extkey = extkey.neuter()

return extkey

ポイントは、

  • seedは128–512 bit , 推奨は 256 bit なのでseedを指定しなかった場合は256 bit のseedを random.getrandbits で生成している
  • derive_from_path ではpathを読み取って鍵を秘密鍵のままderiveして生き、is_privateがfalseの場合は最後にNeuterしている
  • hardened keyの場合はchildindexに2³¹を加算する

次に、ExtKeyのコードです。例外処理はとても適当なので参考にしないでください。

class ExtKey:
def __init__(self, network: str, depth: bytes, fingerprint: bytes, childnumber: bytes, chaincode: bytes, keydata: bytes, is_private: bool):
version = None
if is_private:
if network == "mainnet":
version = bytes.fromhex("0488ADE4")
elif network == "testnet":
version = bytes.fromhex("04358394")

keydata_int = int.from_bytes(keydata, 'big')
if keydata_int == 0 or keydata_int >= n:
raise ValueError("generated key is not valid")

elif not is_private:
if network == "mainnet":
version = bytes.fromhex("0488B21E")
elif network == "testnet":
version = bytes.fromhex("043587CF")
if version is None:
raise BaseException("cannot determine version")

self.network = network
self.version = version
self.depth = depth
self.fingerprint = fingerprint
self.childnumber = childnumber
self.chaincode = chaincode
self.keydata = keydata
self.is_private = is_private

def serialize(self):
ret = bytearray()
ret.extend(self.version)
ret.extend(self.depth)
ret.extend(self.fingerprint)
ret.extend(self.childnumber)
ret.extend(self.chaincode)
if self.is_private:
ret.extend(b"\0")
ret.extend(self.keydata)
return base58.b58encode_check(bytes(ret))

def neuter(self):
if not self.is_private:
print("this is already neutered.")
return self

pubkeydata = PrivateKey(self.keydata, raw=True).pubkey.serialize()

return ExtKey(self.network, self.depth, self.fingerprint, self.childnumber, self.chaincode, pubkeydata, False)

def derive_priv(self, childindex: int, is_hardened: bool):
if not self.is_private:
raise BaseException("cannot derive privatekey from publickey")

par_pub = self.neuter().keydata
ba = bytearray()
if is_hardened:
ba.extend(b"\x00" + self.keydata)
else:
ba.extend(par_pub)

ba.extend(childindex.to_bytes(4, 'big'))
I64 = hmac.HMAC(key=self.chaincode, msg=bytes(ba),
digestmod=hashlib.sha512).digest()

new_priv = (int.from_bytes(I64[:32], 'big') +
int.from_bytes(self.keydata, 'big')) % n
new_priv = new_priv.to_bytes(32, 'big')
depth = int.from_bytes(self.depth, 'big') + 1

new_fingerprint = hashlib.sha256(par_pub).digest()
new_fingerprint = hashlib.new(
"ripemd160", new_fingerprint).digest()[:4]

return ExtKey(self.network, depth.to_bytes(1, 'big'), new_fingerprint, childindex.to_bytes(4, 'big'), I64[32:], new_priv, True)

def derive_pub(self, childindex: int, is_hardened: bool):
if self.is_private:
return self.neuter().derive_pub_from_pub(childindex, is_hardened)
else:
return self.derive_pub_from_pub(childindex, is_hardened)

def derive_pub_from_pub(self, childindex: int, is_hardened: bool):
if is_hardened:
raise BaseException("cannot derive hardened pubkey from pubkey")

ba = bytearray()
ba.extend(self.keydata)
ba.extend(childindex.to_bytes(4, 'big'))
I64 = hmac.HMAC(key=self.chaincode, msg=bytes(ba),
digestmod=hashlib.sha512).digest()

new_priv = (int.from_bytes(I64[:32], 'big') +
int.from_bytes(self.keydata, 'big')) % n
new_priv = new_priv.to_bytes(32, 'big')
depth = int.from_bytes(self.depth, 'big') + 1

new_fingerprint = hashlib.sha256(self.keydata).digest()
new_fingerprint = hashlib.new(
"ripemd160", new_fingerprint).digest()[:4]

return ExtKey(self.network, depth.to_bytes(1, 'big'), new_fingerprint, childindex.to_bytes(4, 'big'), I64[32:], new_priv, True)

ポイントは、

  • derive_privは公開鍵からは呼べない
  • derive_pubを秘密鍵から読んだ場合はNeuterしてから、公開鍵から呼んだ場合はそのままderive_pub_from_pubを呼ぶ
  • 秘密鍵の場合、keydataが0か、secp256k1の位数(コード内ではn)以上の場合はその鍵を用いない

所感

今回は、ブロックチェーンを触ったことがある人なら利用しているであろうHDwalletの仕様を定めたBIP32を実装しました。バイトコードの扱いの勉強としてはちょうどいい難易度だと思うので、いろんな言語で実装するのもいいと思います。

あんまり他のものを参考にせずに自分なりに実装してみたので、改善点などあればコメントください。


お知らせ
■ブロックチェーンエンジニア集中講座開講中!
HashHubではブロックチェーンエンジニアを育成するための短期集中講座を開講しています。お申込み、詳細は下記のページをご覧ください。
ブロックチェーンエンジニア集中講座:https://www.blockchain-edu.jp/
■フレセッツ株式会社では仮想通貨の事業者向け BtoB ウォレットの開発をしていただけるエンジニアを募集しています!詳しくはこちら→https://fressets.com/career/
■HashHubでは入居者募集中です!
HashHubは、ブロックチェーン業界で働いている人のためのコワーキングスペースを運営しています。ご利用をご検討の方は、下記のWEBサイトからお問い合わせください。また、最新情報はTwitterで発信中です。
HashHub:https://hashhub.tokyo/
Twitter:https://twitter.com/HashHub_Tokyo