Pebble Coding

ソフトウェアエンジニアによるIT技術、数学の備忘録

bitcoin ウォレットダンプの値をpythonで計算してみる

ウォレットをテキストファイルに出力する。

$ bitcoin-cli dumpwallet pebble8888wallet.txt
{
  "filename": "/Users/pebble8888/Library/Application Support/Bitcoin/pebble8888wallet.txt"
}
$ cat pebble8888wallet.txt
# Wallet dump created by Bitcoin v0.17.99.0-5da08e0ac
# * Created on 2019-01-10T13:16:40Z
# * Best block at time of backup was 484605 (000000000e8a1b36657d66ad06c51ff4036d5726e027a6d23a07457f8cad5dee),
#   mined on 2015-06-24T03:55:32Z

# extended private masterkey: tprv8ZgxMBicQKsPdhUYUrFmhzLs2poK6jsahfct9ZPsnU4sgtJyLA6bTK4VxC3xBEUkvWVqQ1LDDPrgXG5mQ3EewFx4yyWwFjYN8Hv7sEagUAT

cV3B9EyZX7TpBvDGy3axi7npjjFWk2hToToudxC1tSvoyQdeUJRk 2019-01-10T12:52:19Z reserve=1 # addr=2MwxkoDTAN5KoHhxvnmK7W7dArj6YLWDU2g hdkeypath=m/0'/0'/1'
cS1odU4867PFCRPqjugC4X5eTidS2FVJp3bviyw8p4kb3Vchf92v 2019-01-10T12:52:19Z reserve=1 # addr=2N5j3n62PD8ryM99B46wV96BZx6FBiyPBH1 hdkeypath=m/0'/0'/0'
cUtJMReTcdruToLKNEyNJCeUcUwpJfrzcp36LsJGUNPtxJrTmN2K 2019-01-10T12:52:19Z reserve=1 # addr=2N1wT9jEzdv8wvPneqGdQw2BTAtD3cSEiy9 hdkeypath=m/0'/0'/4'
cVdjw7ce3m4KE1Y5cxX5xPzBVDmNiXNiCZSC1mxpBnpuQhRHQyYX 2019-01-10T12:52:19Z hdseed=1 # addr=2N7hiJovR44NLtZr1szSKiYrNbxo9ArP8RK hdkeypath=s
cU9MWuQ1x6EsEztJWAD5ckYUNiZh5XdbvZgQhzdHXPiLuWqzVMDX 2019-01-10T12:52:19Z reserve=1 # addr=2MudNzAzNiXoVNW5MVpynUgFdw9wVwir5cm hdkeypath=m/0'/0'/2'
cQVwJRTY5aKWp87XhdDBLQc5CZ5CVQDv6VG9aQAhYWD5DtT3aLh8 2019-01-10T12:52:19Z reserve=1 # addr=2NFc21XSHC4SDx6No6X9zCaMXrd693qU5G4 hdkeypath=m/0'/0'/3'
cQuWeTfyx18euSWQdzU7n2E6KY3vuUPC4noimWxFHvT6JG7bf92H 2019-01-10T12:52:20Z reserve=1 # addr=2NCbYjH9y7V7rvLBJu1izGdnUMXXGUL8G1M hdkeypath=m/0'/1'/116'
...

tprv8ZgxMBicQKsPdhUYUrFmhzLs2poK6jsahfct9ZPsnU4sgtJyLA6bTK4VxC3xBEUkvWVqQ1LDDPrgXG5mQ3EewFx4yyWwFjYN8Hv7sEagUAT
の部分が拡張マスタ秘密鍵です。
これはBase58Checkエンコードされており、終端の4バイトはBase58Checkのチェックサムです。
Base58Checkエンコードする前のバイト列のフォーマットは、

VersionPrefix 4バイト+
Depth 1バイト+
FingerPrint 4バイト+
Child 4バイト+
チェーンコード32バイト+
ゼロ固定1バイト+
秘密鍵32バイト

の78バイトです。(BIP32_EXTKEY_SIZE=74)

元の値をpython3で計算してみます。

$ pip3 install base58
$ python3
>>> import base58
>>> b = base58.b58decode_check(b'tprv8ZgxMBicQKsPdhUYUrFmhzLs2poK6jsahfct9ZPsnU4sgtJyLA6bTK4VxC3xBEUkvWVqQ1LDDPrgXG5mQ3EewFx4yyWwFjYN8Hv7sEagUAT')
>>> len(b)
78
>>> b.hex()
'0435839400000000000000000052ece67d787b5e16a1be6e0de0c31d6a56624a2c7e7be6c5b37646a8e5b09a8900e7d7db752a74ef7a74ded86ec654c88e0a23a27ad8e28773a598691ce5a7049e'

したがって、
VersionPrefix: '04358394'
Depth: '00'
FIngerPrint: '00000000'
Child: '00000000'
チェーンコード: '52ece67d787b5e16a1be6e0de0c31d6a56624a2c7e7be6c5b37646a8e5b09a89'
ゼロ固定: '00'
秘密鍵: 'e7d7db752a74ef7a74ded86ec654c88e0a23a27ad8e28773a598691ce5a7049e'
となります。

この秘密鍵から公開鍵を生成するには以下のようにします。

$ python3
>>> k = int.from_bytes(bytes.fromhex('e7d7db752a74ef7a74ded86ec654c88e0a23a27ad8e28773a598691ce5a7049e'), byteorder='big')
>>> k
>>> 104865654782896816244298956663122112459703029562661443056137720684124081423518
sage: E = EllipticCurve(GF(2 ** 256 - 2 ** 32 - 977), [0, 7])
sage: G = E([55066263022277343669578718895168534326250603453777594175500187360389116729240,32670510020758816978083085130507043184471273380659243275938904335757337482424])
sage: k = 104865654782896816244298956663122112459703029562661443056137720684124081423518
sage: k * G
(3502612328192384617976106860973490077358907531062370046409043344360434612976 : 51976356638858599588984936426584047029523293833569662115520287946964673331797 : 1)
>>> x = 3502612328192384617976106860973490077358907531062370046409043344360434612976
>>> y = 51976356638858599588984936426584047029523293833569662115520287946964673331797
>>> print('{:064x}'.format(x))
07be6887e97bf74882ff3bb73aab63f7fe48931bbc7281c5d325606f683cbaf0
>>> print('{:064x}'.format(y))
72e993495a0dcc8ea5ef35a1bb02ba24e5b4ffca7c7b4acdb36618c662818255

yの値が奇数なので、マスター圧縮公開鍵は
M = '0307be6887e97bf74882ff3bb73aab63f7fe48931bbc7281c5d325606f683cbaf0' ということになります。

ウォレットの以下の行以外はaddr=の部分はビットコインアドレスを表しますが、
hdseed=1の行だけはビットコインアドレスではなく、seedを秘密鍵とみなしたときの圧縮公開鍵(先頭が0x02または0x03)をビットコインアドレス形式にしたものです。 seed圧縮公開鍵は、0377fcfd729e8e580a7bf849bed9bbc44d4062e9090df097f34fb2adcb74bfc44f
となっています。
seedの作り方としては、32バイトのランダムな値を生成して、これがxの値として楕円曲線の点となっていない場合は取り直します。

hdseed=1 # addr=2N7hiJovR44NLtZr1szSKiYrNbxo9ArP8RK

マスタ秘密鍵(32バイト)とマスタチェーンコード(32バイト)はseedから一意に決定されます。
seedに対してHMAC-SHA512を取り、前半32バイトがマスタ秘密鍵、後半32バイトがマスタチェーンコードとなります。

seedの値はウォレットダンプからはわかりませんが"f04343f0d4c7396f0655edd6853422a279aebb3314c9dd362b416298b7dacb40"であることが分かっていますので、
seedからマスタ秘密鍵とマスタチェーンコードを算出してみましょう。

import hashlib
import hmac

hex_seed = "f04343f0d4c7396f0655edd6853422a279aebb3314c9dd362b416298b7dacb40"
seed = bytes.fromhex(hex_seed)
key = "Bitcoin seed".encode('utf8')

a = hmac.new(key, seed, hashlib.sha512).hexdigest()
print(a)
e7d7db752a74ef7a74ded86ec654c88e0a23a27ad8e28773a598691ce5a7049e52ece67d787b5e16a1be6e0de0c31d6a56624a2c7e7be6c5b37646a8e5b09a89

一致していることが確かめられました。
seed公開鍵の方も計算してみましょう。

hex_seed = "f04343f0d4c7396f0655edd6853422a279aebb3314c9dd362b416298b7dacb40"
seed = bytes.fromhex(hex_seed)
print(int.from_bytes(seed, 'big'))
108673931323124682021450207138077309184492588200803160682782871684443149093696
sage: k = 108673931323124682021450207138077309184492588200803160682782871684443149093696
sage: k * G
(54272223673690410107067306572649404671112435427396683923283695463496704574543 : 29407153271561851650417279834589029518455454545578285177340103652846543771771 : 1)
x = 54272223673690410107067306572649404671112435427396683923283695463496704574543
print(x.to_bytes(32, 'big').hex())
77fcfd729e8e580a7bf849bed9bbc44d4062e9090df097f34fb2adcb74bfc44f

yの値は奇数なので、seedの拡張公開鍵は"0377fcfd729e8e580a7bf849bed9bbc44d4062e9090df097f34fb2adcb74bfc44f" となります。ここからビットコインアドレスを算出しましょう。

フォーマットはP2WPKH(Pay to Witness Pubkey Hash) となります。
scriptPubKeyは先頭にOP_0を表す0x00(1バイト)、続いて上記の33バイトの公開鍵をhash160をとって20バイトにしたものを、
可変長整数フォーマットにしたものと結合したものをさらにhash160を取り、b58checkを取ったものがビットコインアドレスになります。

可変長整数フォーマットでは、長さが252以下の場合は長さを1バイトで表現したものをデータの先頭につけます。
ここでは長さが20バイトなので 20 = 0x14 を追加します。

python で書くと以下のようになります。

import base58
import hashlib

def b58check(version, payload):
    d = version + payload
    h1digest = hashlib.sha256(d).digest()
    h2digest = hashlib.sha256(h1digest).digest()
    e = d + h2digest[0:4]
    return base58.b58encode(e)

def hash160(ba):
    d = hashlib.sha256(ba).digest()
    m = hashlib.new('ripemd160')
    m.update(d)
    return m.digest()

hex_pubkey = "0377fcfd729e8e580a7bf849bed9bbc44d4062e9090df097f34fb2adcb74bfc44f"
scriptid = hash160(bytes.fromhex(hex_pubkey))
hex_scriptid = scriptid.hex()

l = len(scriptid)
OP_0 = "00"
hex_len = '{:02x}'.format(l)
hex_witprog = OP_0 + hex_len + hex_scriptid
#hex_witprog ="00147a1fccd58db0bcb244c6db4367872bbd6f43d07d" # seed
s = bytes.fromhex(hex_witprog)

#hex_version="00" # mainnet bitcoin address
#hex_version="05" # mainnet P2SH address
#hex_version = "6f" # testnet bitcoin address
hex_version = "c4" # testnet P2SH address
dest_address = b58check(bytes.fromhex(hex_version), hash160(s))
print(dest_address)
2N7hiJovR44NLtZr1szSKiYrNbxo9ArP8RK

一致することが確かめられました。
seed公開鍵のビットコインアドレスは受け取りに使うことも可能ですが、seedと直結しているので、実際に使われることはないでしょう。