LoginSignup
9

More than 3 years have passed since last update.

posted at

updated at

Bitcoin Coreを触りながらBitcoinについて理解する - その1(構築~アドレス生成まで)

概要

Bitcoinをなんとなく知っているけど、なんとなくしか知らない人です。

今回、勉強のためにBitcoinフルノードの運用に取り組んでみたので備忘録として残しておきます。
各種参考にさせていただいた記事のうち代表的なものを随所にリンク張っておきます。
Bitcoin Core(v0.19.0.1)を前提とした内容であることを念頭に置いてください。
あと、勉強しながら平行して記事を書き進めているので、まだ理解が進んでいないポイントは盛大に間違っている可能性があります。

環境

今回使用するマシンのスペック

  • CPU:8 core
  • memory:16GB
  • HDD:1TB

な仮想マシンをVirtualBoxで作成。
OSはCentOS 7です。

1.Bitcoin Coreインストール

OS環境
$ cat /etc/redhat-release 
CentOS Linux release 7.7.1908 (Core)

CentOSの初期設定が終わった状態からスタート。

事前準備
#依存パッケージインストール
yum install -y yum-utils

#拡張リポジトリ(Extra Packages for Enterprise Linux)追加
yum install -y epel-release 

#もろもろ追加(不要なものもあると思うが細かいことは気にしない)
yum install -y gcc gcc-c++ cpp libtool patch patchutils bzip2 zip unzip rsync wget ntp pciutils make autoconf automake libxslt openssl-devel zlib-devel curl-devel bind-utils ncurses ncurses-devel ncurses-static ncurses-term libevent libevent-devel libevent-doc

準備が整ったらrpmからbitcoin-serverbitcoin-utilsをインストールする。

インストール
#リポジトリを設定
rpm -Uvh https://linux.ringingliberty.com/bitcoin/el7/x86_64/bitcoin-release-3-1.noarch.rpm
yum-config-manager --enable rhel-7-server-optional-rpms

# bitcoin coreインストール
yum -y install bitcoin-server bitcoin-utils
バージョン確認
#bitcoin coreのバージョン確認
$ bitcoind -version
Bitcoin Core version v0.19.0.1-g1bc9988993ee84bc814e5a7f33cc90f670a19f6a
Copyright (C) 2009-2019 The Bitcoin Core developers

#bitcoin-cliのバージョン確認
$ bitcoin-cli -version
Bitcoin Core RPC client version v0.19.0.1-g1bc9988993ee84bc814e5a7f33cc90f670a19f6a

2.初期設定

ホームディレクトリ配下に.bitcoinディレクトリを作成し、bitcoin.confファイルを配置する。

初期設定
$ mkdir ~/.bitcoin
~/.bitcoin/bitcoin.conf
### -- 同期するブロックチェーンを指定する -- ###
mainnet=1    ## 本番環境
# testnet=3  ## テストネット(3代目)で試験する場合
# regtest=1  ## オフラインでクローズドな試験をする場合

### -- 動作モード -- ###
server=1     ## JSON RPCサーバとして起動する

### -- RPCポート -- ###
### mainnetなら8332、testnetなら18332がデフォルト値
# rpcport=8332
# rpcport=18332

### -- 認証情報 -- ###
### localhostのみ許可するのであれば凝った設定は不要
rpcuser=user
rpcpassword=pass

### -- アクセス制御 -- ###
### RPCはlocalhostのみから受け付けるのが一般的だが
### 下記設定を追加することにより外部からRPC接続も可能。
rpcallowip=192.168.0.1/32

### -- トランザクション検索インデックスの有効化 -- ###
### トランザクション情報を参照したい場合は必要
txindex=1

(参考)https://qiita.com/Yuto421/items/f790a6c136ee0d530135

設定項目は基本的には以上で完了。
メインネットに接続すると自動的にブロックの同期が始まるため初回はかなりの時間を要するので、もしもお試しや検証目的であればまずはテストネットを使用してみるとよいかと。

3.起動(メインネット接続)

bitcoindを起動しメインネットへの接続を開始。

bitcoind起動
$ bitcoind -daemon
Bitcoin Core starting

デーモンを終了する場合は下記のコマンド

bitcoind停止
$ bitcoin-cli stop
Bitcoin Core stopping

もしうまく動作しないようなら下記の通りRPCまわりからチェックしてみるとよい。

3.1 RPC稼働確認

rpcbindの状態を確認する
$ systemctl status rpcbind

● rpcbind.service - RPC bind service
   Loaded: loaded (/usr/lib/systemd/system/rpcbind.service; enabled; vendor preset: enabled)
   Active: active (running) since 火 2019-12-17 11:57:56 JST; 3 weeks 1 days ago
 Main PID: 1459 (rpcbind)
    Tasks: 1
   CGroup: /system.slice/rpcbind.service
           └─1459 /sbin/rpcbind -w

12月 17 11:57:52 BTC systemd[1]: Starting RPC bind service...
12月 17 11:57:56 BTC systemd[1]: Started RPC bind service.
rpcbindが自動起動設定になっているか確認する
$ systemctl list-unit-files -t service | grep "rpcbind"
rpcbind.service enabled 

$ systemctl list-unit-files | grep "rpcbind"
rpcbind.service enabled 
rpcbind.socket enabled 
rpcbind.target static 
rpcの起動確認
$ rpcinfo -p
program vers proto port service
100000 4 tcp 111 portmapper
100000 3 tcp 111 portmapper
100000 2 tcp 111 portmapper
100000 4 udp 111 portmapper
100000 3 udp 111 portmapper
100000 2 udp 111 portmapper

必要に応じてFWのポート開放も行う。

port開放
$sudo firewall-cmd --zone=manage --add-port=18832/tcp --permanent
$sudo firewall-cmd --zone=manage --add-port=18832/udp --permanent
$sudo firewall-cmd --reload
$sudo firewall-cmd --list-all-zones

3.2 ネットワーク状況確認

bitcoinネットワークの接続状況を確認する。

TCPコネクション確認
$ netstat -pant | grep bitcoin

tcp 0 0 127.0.0.1:18332 0.0.0.0:* LISTEN 12531/bitcoind 
tcp 0 0 0.0.0.0:18333 0.0.0.0:* LISTEN 12531/bitcoind 
tcp 0 0 192.168.1.1:44054 xxx.xxx.xxx.xxx:18333 ESTABLISHED 12531/bitcoind 
<snip>
tcp6 0 0 ::1:18332 :::* LISTEN 12531/bitcoind 
tcp6 0 0 :::18333 :::* LISTEN 12531/bitcoind 

ローカルノードの内部情報を参照するにはbitcoin-cliコマンドを使用する。

ネットワークコネクション数
$ bitcoin-cli getconnectioncount
10
ネットワーク接続状況
$ bitcoin-cli getnetworkinfo
{
  "version": 190001,
  "subversion": "/Satoshi:0.19.0.1/",
  "protocolversion": 70015,
  "localservices": "0000000000000409",
  "localservicesnames": [
    "NETWORK",
    "WITNESS",
    "NETWORK_LIMITED"
  ],
  "localrelay": true,
  "timeoffset": 0,
  "networkactive": true,
  "connections": 10,
  "networks": [
    {
      "name": "ipv4",
      "limited": false,
      "reachable": true,
      "proxy": "",
      "proxy_randomize_credentials": false
    },
    {
      "name": "ipv6",
      "limited": false,
      "reachable": true,
      "proxy": "",
      "proxy_randomize_credentials": false
    },
    {
      "name": "onion",
      "limited": true,
      "reachable": false,
      "proxy": "",
      "proxy_randomize_credentials": false
    }
  ],
  "relayfee": 0.00001000,
  "incrementalfee": 0.00001000,
  "localaddresses": [
  ],
  "warnings": ""
}

3.3 ローカルノードの稼働状況確認

動作バージョンやブロックチェーン、ウォレット残高などの概況サマリーを確認する。

サマリー情報
$ bitcoin-cli -getinfo
{
  "version": 190001,
  "protocolversion": 70015,
  "blocks": 611853, ## 最新ブロック高
  "timeoffset": 0,
  "connections": 10,
  "proxy": "",
  "difficulty": 13798783827516.42, ## 採掘難易度
  "chain": "main",
  "walletversion": 169900,
  "balance": 0.00000000,  ## wallet残高
  "keypoololdest": 1576991493,
  "keypoolsize": 1000,
  "unlocked_until": 0, ## wallet複合時の終了期限(暗号化済みの場合に項目表示)
  "paytxfee": 0.00000000,
  "relayfee": 0.00001000,
  "warnings": "" ## 警告メッセージ
}

3.4 ブロックチェーン同期状況の確認

Bitcoinネットワークと接続されれば自動的にネットワーク上の最新のブロックチェーンとのローカル同期が始まります。
初回起動時は初期ブロック(genesisブロック)から最新ブロックまでの全ブロックを順次ダウンロード&検証するのでだいたい20時間以上は見ておいたほうがいいです。

同期状況の進捗はgetblockchaininfoというRPCコマンドにより確認可能。
blocksheadersが同値になれば同期完了ということらしい。

ブロックチェーン同期状況
$ bitcoin-cli getblockchaininfo
{
  "chain": "main",
  "blocks": 615023,  ## ローカル同期が完了したブロック長
  "headers": 615023,  ## ネットワーク上の最新ブロック長
  "bestblockhash": "0000000000000000000597fb45aa1f4ca78081352cb2d26487ba7d019bd76a08",
  "difficulty": 15466098935554.65,
  "mediantime": 1580285702,  ## タイムスタンプ(中央値)
  "verificationprogress": 0.9999997039397914,
  "initialblockdownload": false,
  "chainwork": "00000000000000000000000000000000000000000c2d3de90e0990276f7b9f70",
  "size_on_disk": 295504268933,
  "pruned": false,
  "softforks": {
    "bip34": {
      "type": "buried",
      "active": true,
      "height": 227931
    },
    "bip66": {
      "type": "buried",
      "active": true,
      "height": 363725
    },
    "bip65": {
      "type": "buried",
      "active": true,
      "height": 388381
    },
    "csv": {
      "type": "buried",
      "active": true,
      "height": 419328
    },
    "segwit": {
      "type": "buried",
      "active": true,
      "height": 481824
    }
  },
  "warnings": ""
}

進捗状況の確認はblocksでもざっくりわかるが、より具体的に知りたい場合は、ブロックのタイムスタンプmediantimeを確認できる。タイムスタンプはUNIX時間で与えられるのでdateコマンドを用いれば読める。

$ date --date @1580285702
2020年  1月 29日 水曜日 17:15:02 JST

mediantimeは同期が完了した最終ブロックに含まれるタイムスタンプの一つなのだが、厳密にはブロックの生成日時ではなく、当該ブロックを含む過去11ブロックの生成日時の中央値を示しているらしい。

https://bitcoin.stackexchange.com/questions/67618/difference-between-time-and-mediantime-in-getblock

通常1ブロックの生成間隔は凡そ10分なのでmediantimeはブロックの生成日時から概ね1時間程度過去の値として算出されることになる。Bitcoinネットワークのハッシュレートは割と変動するし、分岐も頻繁に発生しているので、そういった不確定要因を極力除外したいとの思想から平均値ではなく中央値を利用しているのかな?
多少余談になるが難易度調整アルゴリズムについては下記のレポートが勉強になった。

Bitcoin の難易度調整アルゴリズムの欠陥と、その解決策について

同期完了後のディスク使用量

実際にサーバのディスク容量をどの程度使うのかは気になるところ。
.bitcoinディレクトリのディスク使用量を確認すると2020年1月執筆時点で300GBを超えており、そのうち90%以上がブロックチェーン本体のバイナリデータで占められている模様だった。

ディスク使用量
$ du -h
93M     ./blocks/index
274G    ./blocks
3.7G    ./chainstate
24G     ./indexes/txindex
24G     ./indexes
60K     ./database
301G    .

4.ブロックやトランザクションの中身を調べてみる

同期が完了したので、とりあえず過去のブロックやトランザクションがどうなっているか覗いてみた。
ここでは他サイトで紹介されていた方法を真似しているが他にもやり方があるかもしれない。

4.1 ブロックの中身を確認する

参照したいブロックを指定するには各ブロックに一意なブロックハッシュ値blockhashを指定する必要がある。ハッシュ値が分からなければ(普通はそんなもの覚えているわけがない・・・)、ブロックチェーン上の位置heightを指定すれば、そのブロックに対応するブロックハッシュ値を返してくれるRPCコマンドが別に用意されているので、それを併用するようだ。具体的には
 getblockhash <height>:heightに対応するブロックのブロックハッシュ値を特定する
 getblock <blockhash>:ブロックハッシュ値に対応するブロック情報を表示する
という2段階を経ることになる。

例として100001番目に位置するブロックを取得した実行結果を示す。
(heightは0始まりであることに注意/ genesis=0)

ブロック情報出力 実行例
# height=100000のブロックハッシュを特定する
$ bitcoin-cli getblockhash 100000
000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506

# 上記ブロックハッシュに対応するブロックの中身を表示する
$ bitcoin-cli getblock 000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506
{
  "hash": "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506",
  "confirmations": 511995,
  "strippedsize": 957,
  "size": 957,
  "weight": 3828,
  "height": 100000, ## ブロックHeight
  "version": 1,
  "versionHex": "00000001",
  "merkleroot": "f3e94742aca4b5ef85488dc37c06c3282295ffec960994b2c0d5ac2a25a95766",
  "tx": [
    "8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87",
    "fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4",
    "6359f0868171b1d194cbee1af2f16ea598ae8fad666d9b012c8ed2b79a236ec4",
    "e9a66845e05d5abc0ad04ec80f774a7e585c6e8db975962d069a522137b80c1d"
  ],
  "time": 1293623863,
  "mediantime": 1293622620,
  "nonce": 274148111,
  "bits": "1b04864c",
  "difficulty": 14484.1623612254,
  "chainwork": "0000000000000000000000000000000000000000000000000644cb7f5234089e",
  "nTx": 4,  ## トランザクション数
  "previousblockhash": "000000000002d01c1fccc21636b607dfd930d31d01c3a62104612a1719011250",
  "nextblockhash": "00000000000080b66c911bd5ba14a74260057311eaeb1982802f7010f1a9f090"
}

これは2010年頃のブロックになるのでトランザクションは僅か4件しか記録されていない。こういった黎明期を維持してくれた先駆者に感謝の思いを馳せる。
一点補足すると、ここに出力されている情報のうち実際のブロックチェーン上のブロックには記載されていないものがある。分かりやすい例としては末尾のnextblockhashがそうだ。これは次のブロックのブロックハッシュを示しているが、ブロックが実際に生成(マイニング)された時点でまだ存在しない未来のブロックのハッシュ値が確定できるはずもない。すなわちこれはビットコインソフトウェアがブロックチェーンを解析して独自に付与しているということを意味している。

TIPS

getblockhashからgetblockを実行するコンボはこれからもよく使いそうなのでシェルスクリプト化しておくと便利

getblk.sh
#!/bin/bash
### heightを引数に指定しブロック情報を出力する
blockhash=`bitcoin-cli getblockhash $1`
bitcoin-cli getblock $blockhash

その他、最新ブロックの情報を取得するコマンド

## 最新ブロックの位置を特定する
$ bitcoin-cli getblockcount
615022

## (または)最新ブロックのブロックハッシュを特定する
$ bitcoin-cli getbestblockhash
0000000000000000000074bc934c66bccb7e1963d1376e46613f715b13077045

4-2.トランザクションの中身を確認する

次にトランザクションの中身を覗いてみる。
先ほどブロック情報を表示した際にtxセクションが含まれていたが、この文字列がそれぞれのトランザクションを特定するトランザクションIDtxidなるものらしい。

(再掲)
  "tx": [
    "8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87",
    "fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4",
    "6359f0868171b1d194cbee1af2f16ea598ae8fad666d9b012c8ed2b79a236ec4",
    "e9a66845e05d5abc0ad04ec80f774a7e585c6e8db975962d069a522137b80c1d"
  ],

トランザクションの中身を表示したい場合はtxidでインデックスを検索してrawデータを取得し、それをデコードするという手順となる。具体的には下記のRPCコマンドを用いる。
 getrawtransaction <txid>:txidからrawデータを取得する
 decoderawtransaction <raw>:rawデータをデコードする

トランザクション情報出力 実行例

## txidからrawデータを取得する
$ bitcoin-cli getrawtransaction 8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87
01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4444acb83c4ec7a0e2f99dd7457516c5817242da796924ca4e99947d087fedf9ce467cb9f7c6287078f801df276fdf84ac00000000

## rawデータをデコードする
$ bitcoin-cli decoderawtransaction 01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4444acb83c4ec7a0e2f99dd7457516c5817242da796924ca4e99947d087fedf9ce467cb9f7c6287078f801df276fdf84ac00000000
{
  "txid": "8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87",
  "hash": "8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87",
  "version": 1,
  "size": 135,
  "vsize": 135,
  "weight": 540,
  "locktime": 0,
  "vin": [
    {
      "coinbase": "044c86041b020602",
      "sequence": 4294967295
    }
  ],
  "vout": [
    {
      "value": 50.00000000,
      "n": 0,
      "scriptPubKey": {
        "asm": "041b0e8c2567c12536aa13357b79a073dc4444acb83c4ec7a0e2f99dd7457516c5817242da796924ca4e99947d087fedf9ce467cb9f7c6287078f801df276fdf84 OP_CHECKSIG",
        "hex": "41041b0e8c2567c12536aa13357b79a073dc4444acb83c4ec7a0e2f99dd7457516c5817242da796924ca4e99947d087fedf9ce467cb9f7c6287078f801df276fdf84ac",
        "type": "pubkey"
      }
    }
  ]
}

vinがインプット情報(出金元UTXO)
voutがアウトプット情報(送金先UTXO)

この出力結果にはvinセクションにcoinbaseという記述があるが、これはマイニングの報酬として新たに生み出されたコインであることを意味している。つまりこのトランザクションは『採掘報酬50BTCを採掘者に送付しますよ』という取引であることがわかる。

こういった情報はBlockchain Explorerと呼ばれるWEBサイトにも公開されているので併せて確認することで理解が深まると思う。
(https://www.blockchain.com/btc/tx/8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87)

TIPS

トランザクションについてもブロックと同じくgetrawtransactionからdecoderawtransactionを実行するコンボは有用なのでシェルスクリプト化しておくとよい。

gettx.sh
#!/bin/bash
### トランザクションIDを引数に指定しトランザクション情報を出力する
txid=`bitcoin-cli getrawtransaction $1`
bitcoin-cli decoderawtransaction $txid

5.ウォレットの管理

次はウォレットやアドレスの仕組みについて確認していきたい。この章あたりから、いよいよビットコインの真髄が見えてきそうだ。

ウォレットの実体は.bitcoinディレクトリ直下にあるwallet.datというバイナリファイルである。getwalletinfoコマンドで内容の確認ができる。

ウォレット情報
$ bitcoin-cli getwalletinfo
{
  "walletname": "",
  "walletversion": 169900,
  "balance": 0.00000000, ## BTC残高
  "unconfirmed_balance": 0.00000000,
  "immature_balance": 0.00000000,
  "txcount": 0,
  "keypoololdest": 1576991493,
  "keypoolsize": 1000,
  "keypoolsize_hd_internal": 1000,
  "unlocked_until": 0, ## 暗号化のアンロック期限
  "paytxfee": 0.00000000,
  "hdseedid": "2d819a2d1265e61aeb1dfa80e53f1c0d07536ccb",
  "private_keys_enabled": true,
  "avoid_reuse": false,
  "scanning": false
}

初期状態ではunlocked_untilの項目が存在しない。これは暗号化walletを一時的にアンロックした場合にその期限を表示するためのフィールドである(0の場合は暗号化されていることを意味する)。walletを暗号化すれば項目が出現する。

5-1. wallet暗号化/複合化

walletを暗号化するためにはencryptwalletを用いる

wallet暗号化
$ bitcoin-cli encryptwallet <passphrase>

暗号化することでパスフレーズによってそのウォレットは保護され秘密鍵が秘匿される。このサーバに不正アクセスされたとしてもパスフレーズが漏洩しなければ勝手に送金されてしまうといった事態が避けられる。
複合化したい場合は制限時間を設けて時限的に複合化することができる。

wallet複合化(制限時間を120秒に指定する場合)
$ bitcoin-cli walletpassphrase <passphrase> 120

(参考)https://bitcoin.clock-up.jp/contents/wallet/passphrase

5-2. ビットコインアドレスの作成

ビットコインアドレスはBTCを受け取るための口座番号みたいなもの。
これはいくらでも好きなだけ(!)自分で生成することができる。
アドレス生成にはgetnewaddressコマンドを用いる。
またアドレスには識別子として任意のラベルlabelを付けることができる。これはあくまでローカルだけで通用するものであり、アドレスとラベルを紐付けることで人間が管理しやすくするためのものである。一つのラベルに複数のアドレスを紐付けることも可能である。

ビットコインアドレスの生成
## ラベル:hoge11に2つ、ラベル:hoge22に1つのアドレスをそれぞれ生成する
$ bitcoin-cli getnewaddress hoge11
3Lr2tGo21MLmR8De4Jw1dA8zhDdPy2wRbi

$ bitcoin-cli getnewaddress hoge11
35YzatN8GgtGFBpjT7HWkZHNgcKQ2FmHss

$ bitcoin-cli getnewaddress hoge22
3Do3Uenc4WwZ5pgDBANhDfpZHbwN25hWFg

作成したアドレスはgetaddressbylabelメソッドで確認できる。

ラベル毎に紐付けたアドレス一覧を確認
$ bitcoin-cli getaddressesbylabel hoge11
{
  "35YzatN8GgtGFBpjT7HWkZHNgcKQ2FmHss": {
    "purpose": "receive"
  },
  "3Lr2tGo21MLmR8De4Jw1dA8zhDdPy2wRbi": {
    "purpose": "receive"
  }
}

$ bitcoin-cli getaddressesbylabel hoge22
{
  "3Do3Uenc4WwZ5pgDBANhDfpZHbwN25hWFg": {
    "purpose": "receive"
  }
}

ラベル一覧を表示する:listlabels

ラベル一覧の表示
$ bitcoin-cli listlabels
[
  "hoge11",
  "hoge22",
]

アドレスを生成するということは、それに対応する秘密鍵と公開鍵も同時に生成しそれらを自らのウォレットで保管し管理していくということに等しい。
ここらへんはかなり重要なポイントになってくるのでしっかり勉強しておく。

秘密鍵と公開鍵とビットコインアドレスの関係

ブロックチェーンとトランザクションを銀行に例えて

 - ブロックチェーン = 全口座の全ての入出金履歴
 - トランザクション = 1回の取引記録

というイメージであるとすると、3者(秘密鍵、公開鍵、ビットコインアドレス)の関係性は

 - 秘密鍵 = 暗証番号
 - 公開鍵 = 口座番号(入出金兼用)
 - ビットコインアドレス = 口座番号(入金専用)

という表現が適切だろうか。公開鍵ビットコインアドレスはいずれも形を変えただけで本質は同一の口座を指し示す識別子として利用されるが以下の違いがある。公開鍵またはビットコインアドレスを指定すれば誰でも入金(その口座に送金)することはできるのに対し、出金(別の口座に送金)するためにはビットコインアドレスではなく必ず公開鍵で指定する必要がある。
また出金する際には暗証番号として秘密鍵を用いたデジタル署名で取引書類にサインする必要がある。

なお入金に公開鍵を指定する方法(P2PK=Pay to Public Key)はかなり初期こそ使われていたが、安全性などの理由から新たにビットコインアドレスで指定する方法(P2PKH=Pay to Public Key Hash)が開発され、現在ではP2PKを利用する動機はほとんど無い。ここらへんについては、別途P2SHSeqwitなどと併せて整理したい。

ビットコインソフトウェアがビットコインアドレスを生成する際、内部的には下記の処理が行われている。

 1.疑似乱数を用いて秘密鍵(一般的に256bitの数値)を生成する
 2.ECDSA(楕円曲線デジタル署名アルゴリズム)を用いて秘密鍵から公開鍵を求める
 3.ハッシュ関数を用いて公開鍵からビットコインアドレスを生成する

 1から2、2から3の処理は不可逆的であり、アドレスから公開鍵を求めたり、公開鍵から秘密鍵を求めたりすることは数学的困難さを伴う。この特性を利用してビットコインを安全にやり取りする仕組みが作られている。

秘密鍵(Private Key)

 秘密鍵はそれから生成される公開鍵ならびにビットコインアドレスの唯一の所有者であることを証明するもの。
 秘密鍵はそれ自体は乱数などから生成された任意の数値だが、ビットコインソフトウェアにおいてはそこにいくつかの属性情報(プレフィックス、チェックサムなど)を付加して以下のいずれかの形式で定義される。

 - WIF形式・・・・・先頭文字が5
 - WIF圧縮形式・・・先頭文字がKorL

 WIFというのはWallet Import Formatというビットコイン独自のフォーマット。Base58というビットコインのために定義された58種のキャラクタセットを用いて58進数の数値に変換することで、人間が誤読しにくいようにという配慮がされている。
 圧縮形式というのは後述する公開鍵が非圧縮タイプであるか圧縮タイプであるかによって確定的に決まる。

 試しに先ほど作成したアドレス35YzatN8GgtGFBpjT7HWkZHNgcKQ2FmHssの元となった秘密鍵を確認してみる。秘密鍵を表示するにはdumpprivkeyコマンドを用いる。もちろん自らの保有する秘密鍵でなければ確認することはできない。

秘密鍵の表示
## walletを60秒間だけ複合化する(※秘密鍵にアクセスする際には事前に暗号化を解いておく必要があるため)
$ bitcoin-cli walletpassphrase <passphrase> 60

## アドレスに対応する秘密鍵を表示
$ bitcoin-cli dumpprivkey 35YzatN8GgtGFBpjT7HWkZHNgcKQ2FmHss
KzUwAsGmHLKrpp9dK4SNDiu6DucckKq4uLThjx8grZfaLe6be5he

 Kから始まっているためWIF圧縮形式であることがわかる。
 この記事をご覧になる方にはもはや釈迦に説法だと思うが、秘密鍵は暗証番号に相当するものなので、通常は絶対に秘匿しておかなければならないし、仮に紛失してしまうと、二度とその口座にアクセスすることは叶わないと思った方が良い。実際に十分な強度の256bit秘密鍵を用いている場合、その値がとりうる選択肢は2^256≒10^77となり、これは観測可能な宇宙に存在する全ての素粒子の数(10^80)より少しばかり少ないくらいの数となると言われている。文字通り、万が一にも見つけようと思って見つけられるような次元の話ではないのである。

公開鍵(Public Key)

 公開鍵は秘密鍵(32byte)を楕円曲線デジタル署名アルゴリズム(ECDSA)により変換した2軸の座標(整数X,整数Y)として求められる。ECDSAにも各種方式があり、ビットコインでは米国国立標準技術研究所(NIST)が標準化したSecp256k1という曲線ならびにパラメータを使用している。ビットコインプロトコルにおいて公開鍵は下記の2種類の形式のいずれかを用いることとされている。

 - 非圧縮形式・・・先頭文字が0x04、65byte長、(X,Y)を利用
 - 圧縮形式・・・・先頭文字が0x02or0x03、33byte長、Xのみを利用

 座標(X,Y)は楕円曲線上にあるので、多項式を計算すればXからYは簡単に導出できる関係にある。よって(X,Y)の組を公開鍵として扱うことと、Xのみを公開鍵として扱うことは暗号強度的には等価であり違いはない。当初のビットコインソフトウェアは(X,Y)の組を公開鍵として扱っていたが、トランザクションの容量を削減する目的で、公開鍵の長さが約半分で済むXのみを公開鍵として扱う方式も仕様追加された。これらの経緯から、前者を非圧縮形式、後者を圧縮形式と呼んで区別している。

(参考)【Mastering Bitcoin】秘密鍵から楕円曲線暗号を使って公開鍵を生成する
(参考)【Mastering Bitcoin】 ビットコインアドレスについて(圧縮形式など)

 具体的に公開鍵を確認してみる。先ほどに引き続きアドレス35YzatN8GgtGFBpjT7HWkZHNgcKQ2FmHssに対応する公開鍵を表示するためgetaddressinfoコマンドを使用する。このコマンドを利用するとアドレスの情報が一覧表示されるためここではひとまず公開鍵の部分だけをピックアップする。

公開鍵の表示
$ bitcoin-cli getaddressinfo 35YzatN8GgtGFBpjT7HWkZHNgcKQ2FmHss | grep "pubkey"
  "pubkey": "022d0c1a116d24cef338f53071edae8177901010085f93f170f1d577dd3f6f06d6",
    "pubkey": "022d0c1a116d24cef338f53071edae8177901010085f93f170f1d577dd3f6f06d6",

 先頭文字が0x02なので圧縮形式を使用していることがわかる。これは先ほど確認した秘密鍵もWIF圧縮形式となっていたことと連動している。 圧縮形式においてはプレフィックス1byte(ここでは0x02)の後に続く32byte(2d0c1a116d24cef338f53071edae8177901010085f93f170f1d577dd3f6f06d6)が公開鍵の片割れである座標値Xである。
なお上記実行例で公開鍵が2回表示されている理由についてはここでは割愛する(後述のSegwitラッピングアドレスに関連)。

 参考までに非圧縮形式も確認しておく。非圧縮形式がメインで使われていた最初期のトランザクションを出力してみたい。
 先ほど作成したシェルスクリプトを使用する。

2番目のブロックに含まれるトランザクションの内容を確認する
## 2番目(Height=1)のブロック情報を出力
$ ./getblk.sh 1
{
  "hash": "00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048",
  "confirmations": 614991,
  "strippedsize": 215,
  "size": 215,
  "weight": 860,
  "height": 1,
  "version": 1,
  "versionHex": "00000001",
  "merkleroot": "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098",
  "tx": [
    "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098" ## トランザクションID
  ],
  "time": 1231469665,
  "mediantime": 1231469665,
  "nonce": 2573394689,
  "bits": "1d00ffff",
  "difficulty": 1,
  "chainwork": "0000000000000000000000000000000000000000000000000000000200020002",
  "nTx": 1,
  "previousblockhash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
  "nextblockhash": "000000006a625f06636b8bb6ac7b960a8d03705d1ace08b1a19da3fdcc99ddbd"
}

## トランザクションIDからトランザクション情報を出力
$ ./gettx.sh 0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098
{
  "txid": "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098",
  "hash": "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098",
  "version": 1,
  "size": 134,
  "vsize": 134,
  "weight": 536,
  "locktime": 0,
  "vin": [
    {
      "coinbase": "04ffff001d0104",
      "sequence": 4294967295
    }
  ],
  "vout": [
    {
      "value": 50.00000000,
      "n": 0,
      "scriptPubKey": {
        "asm": "0496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858ee OP_CHECKSIG",
        "hex": "410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac",
        "type": "pubkey"
      }
    }
  ]
}

これはgenesisブロックの次のブロックであり、まだまだ取引らしい取引はなく、coinbaseトランザクションだけが記録されている状況だ。このトランザクションのvoutの中にあるscriptPubKeyに送金先の情報が含まれている。詳しくは別途、トランザクションスクリプトという項目で整理したいと思っているが、ここではasmの項目に注目すると、0x04に続く長い64byte(96b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858ee)が見つかる。これが非圧縮形式で記述された公開鍵である。座標(X,Y)の両方が含まれているので1+32×2=65byteもの情報量になってしまうわけである。
トランザクション容量の圧縮はビットコインの処理性能に深く関わってくる課題なので、圧縮形式の採用もそうだが、後述するビットコインアドレスSegwitの発明へと繋がっていく。こういった歴史的背景を紐解きつつ、理解していくことは非常に重要なことだと思う。

ビットコインアドレス

 ビットコインアドレスは公開鍵をハッシュ演算して導出したものであり、その実体は公開鍵ハッシュと呼ばれることも多い。前項からの文脈で表現するならば、公開鍵の代わりに『公開鍵をハッシュ化したもの』をscriptPubKeyに指定しようぜ、という発想でできたものがであるから公開鍵ハッシュという呼び方は非常にしっくりくる。そして公開鍵ハッシュにプレフィックスやチェックサムなどの属性情報を付加してBase58エンコードしたものを便宜的にビットコインアドレスと呼んでいるわけである。

 ハッシュ演算は具体的には下記の順序で行われる。

公開鍵から公開鍵ハッシュを導出する過程(HASH160)
  公開鍵 → SHA256(256bit) → RIPEMD160(160bit) → 公開鍵ハッシュ(160bit)

 このように2回ハッシュ演算を行うことを一般的にダブルハッシュと呼び、ビットコインではHASH160と表記したりする。なぜ2回もハッシュ化する必要があるのかという疑問が沸くが、採用した理由についてはどうやらサトシ・ナカモトも言及していないようだが、いくつかの参考資料ではLength Extension Attack (長さ拡張攻撃)に対する対策ではないかとの推測が見られた。つまるところハッシュ関数が将来的に部分的に破られたときのリスクに備えて、二重化することで強度を高めておくという事前の予防策、と考えるのが妥当なところだろうか。

(参考)Hashing or encrypting twice to increase security?

 公開鍵ハッシュができたら、プレフィックスとチェックサムを付加するのだが、チェックサムは公開鍵ハッシュをさらにSHA256SHA256ダブルハッシュする(これをSHA256dと呼ぶ)。

公開鍵ハッシュからチェックサムを導出する過程(SHA256d)
  {プレフィックス+公開鍵ハッシュ} → SHA256(256bit) → SHA256(256bit) → 先頭4byteをチェックサムとする

 とにかく随所でハッシュ関数が活躍しまくっている。最後にまとめてBase58エンコードにかけて完成。

ビットコインアドレスの完成
  {プレフィックス(1byte)+公開鍵ハッシュ(20byte)+チェックサム(4byte)}→ Base58エンコード(27~34文字)

 詳細については参考資料の方が100倍わかりやすいのでここでは概要を追いかける。

(参考)秘密鍵から公開鍵とビットコインアドレスを生成する方法

 最終的にビットコインアドレス1or3で始まる27~34文字の英数字として表記される。
 それでは実際に最初に作成したアドレス35YzatN8GgtGFBpjT7HWkZHNgcKQ2FmHssの詳細情報を確認してみたい。
 getaddressinfoコマンドを使うと

アドレスの詳細情報
$ bitcoin-cli getaddressinfo 35YzatN8GgtGFBpjT7HWkZHNgcKQ2FmHss
{
  "address": "35YzatN8GgtGFBpjT7HWkZHNgcKQ2FmHss",
  "scriptPubKey": "a9142a5c0ec9cefb797bdef84b3e57ee8561c40b450987",
  "ismine": true,
  "solvable": true,
  "desc": "sh(wpkh([2f0ff834/0'/0'/9']022d0c1a116d24cef338f53071edae8177901010085f93f170f1d577dd3f6f06d6))#6hhgrklp",
  "iswatchonly": false,
  "isscript": true,
  "iswitness": false,
  "script": "witness_v0_keyhash",
  "hex": "00140797b322026a926da788b9396240556e08ecb217",
  "pubkey": "022d0c1a116d24cef338f53071edae8177901010085f93f170f1d577dd3f6f06d6",
  "embedded": {
    "isscript": false,
    "iswitness": true,
    "witness_version": 0,
    "witness_program": "0797b322026a926da788b9396240556e08ecb217",
    "pubkey": "022d0c1a116d24cef338f53071edae8177901010085f93f170f1d577dd3f6f06d6",
    "address": "bc1qq7tmxgszd2fxmfughyukysz4dcywevshqpjf7w",
    "scriptPubKey": "00140797b322026a926da788b9396240556e08ecb217"
  },
  "label": "hoge11",
  "ischange": false,
  "timestamp": 1576991492,
  "hdkeypath": "m/0'/0'/9'",
  "hdseedid": "2d819a2d1265e61aeb1dfa80e53f1c0d07536ccb",
  "hdmasterfingerprint": "2f0ff834",
  "labels": [
    {
      "name": "hoge11",
      "purpose": "receive"
    }
  ]
}

 ここに記述されている内容を完全に理解するにはまだ学習ステップが足りていないことがよくわかる。。

 とはいえ、こうして定義されたビットコインアドレスは送金先情報としてトランザクションのscriptPubKeyに利用できる運びとなった(P2PKH(Pay to Public Key Hash))。これには主に2つのメリットがあったとサトシ・ナカモトが言及している。

 ①(受け取りの時点では)自身の公開鍵を開示する必要がなくなり、公開鍵暗号への攻撃耐性が向上した。
   量子コンピュータでショアのアルゴリズムを用いると、多項式時間で公開鍵から秘密鍵の導出ができてしまうと言われており、公開鍵が公知になっている状態はそれ自体が将来的なリスク要因となり得る。送金先情報としてビットコインアドレスを用いると公開鍵は秘匿できる。また出金時は残高をお釣りアドレスに転送するような使い捨てタイプのウォレット実装も進んでいるので、これらを併用すれば、実質的に公開鍵暗号への攻撃を無力化できると言える。

 ②文字数圧縮(256bit→160bit)により、可読性の向上であったり、QRコードへの搭載が簡易になった。
   主に実用面への寄与も大きく、ビットコインの普及拡大に貢献したことは間違いないだろう。

(参考)P2PKH (Pay to Public Key Hash)

 ところでここで一つの疑問が沸いてくる。見てきたようにビットコインアドレスの実体は公開鍵ハッシュであり、これは言い換えれば公開鍵の情報量(圧縮形式で256bit)を160bitに弱体化させただけのものだ。先ほど秘密鍵は宇宙の素粒子の数だけあると書いたが、100bit近くも減らしたということは、逆に考えると、同じハッシュを導出する秘密鍵・公開鍵ペアの数が100bit近く増加したと考えることができる。本来、全く無関係なはずの秘密鍵・公開鍵ペアにも関わらずそれらは同一のビットコインアドレスへのアクセス権を有している。こういった衝突が起こってしまう確率はどのくらいあるのだろうか?
 検索してみる一応文献に辿り着いた(権利関係が微妙なのでリンクは割愛)。誕生日のパラドックスという一度は聞いたことがあるアレと同じ問題と捉えると、『1.7×10^23個のアドレスを生成した時点で、いずれかのアドレスが衝突する確率が1%を超える』という試算結果らしい。これは桁数だけ見ればまだ現実的な脅威と感じるかもしれないが、それでも大概大きな数値であり、そうそう起こりえないことは確かである。また、衝突するのが『自分のアドレス』に限定するなら、その確率は天文学的なものになるため、暗号学的には十分な強度があると言えると思う。
 いずれにせよこれに対する解決策は既に提示されており、scriptPubKeyに公開鍵やアドレスを直接指定するのではなく、スクリプトを指定してしまおうというP2SH(Pay to Script Hash)というどこかの頭の良い人が考えた仕組みが既に実装されており、広く使われている。次章ではここらへんに踏み込んでいきたい。

というわけで、キリが良いのでここらで一度締めて投稿します。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
9