Develop with pleasure!

福岡でCloudとかBlockchainとか。

Bitcoin Coreのテストコードの実行方法

既存のコードの追加・改修の際に、動作を保証するテストコードがあり、自動テストできる環境があるのは重要で、Bitcoin Coreにももちろんテストコードと自動テストの仕組みがある。

Bitcoin Coreには単体テストと、リグレッション及びインテグレーションテスト用のテストコードがそれぞれ存在する。

単体テストの実行

テストコードはconfigureオプションで無効化していない限り、自動的にコンパイルされる。

実行対象のテストコードは、以下のMakefileに記載されている↓

(他にもウォレットやsecp256k1などのテストが各フォルダにある。)

テストの実行は簡単で make checkを実行すればC++で書かれたテストが実行される。もしくは、コンパイルされたsrc/test/test_bitcoinを実行してもいい。

$ ./src/test/test_bitcoin    
Running 258 test cases...

*** No errors detected

bitcoin-qtの場合はsrc/qt/test/test_bitcoin-qtを実行する。

個別のテストの実行

↑では全テストコードが実行されるので、個別に実行したい場合は、↓のように--run_testオプションで対象のテストを指定して、src/test/test_bitcoinを使って実行する。

$ ./src/test/test_bitcoin --run_test=script_tests --log_level=all

↑はsrc/test/script_tests.cpp のテスト実行。--log_levelオプションでログをコンソールに出力することもできる。

指定可能なオプションは--helpオプションを指定してtest_bitcoinを実行すれば確認できる。

テストコードの追加

単体テストのコードはboostのテストフレームワークを使って書かれてるので、テストコードを追加する際もそれを使用する。

既存のファイルにテストコードを追加する場合は、既存のテストファイルのBOOST_AUTO_TEST_CASE関数内に追加するか、BOOST_AUTO_TEST_CASE関数を追加して実装する。

新規にテストコードのファイルを追加する場合は、そのファイルを↑のMakefile.test.includeに追記する。
基本的にはソースファイルやクラス毎に1つテストファイルを作成することになっており、テストファイルの命名規則<source_filename>_tests.cpp

bitcoin-qtのテストはsrc/qt/test/以下に追加)

リグレッション及びインテグレーションテストの実行

このテストは↓のセットで構成される。

  • functional
    RPCやP2Pインターフェースを介して動作するbitcoind及びbitcoin-qtの機能をテストするコード。
  • util
    Bitcoinのユーティリティのテストコードで、現在の対象はbitcoin-txのみ。

functionalテストの実行

各テストの実行は、それぞれpythonのテストファイルを直接実行すればいい。

$ test/functional/segwit.py 
2017-08-10 06:51:43.129000 TestFramework (INFO): Initializing test directory /tmp/test92n84pq5
2017-08-10 06:51:45.901000 TestFramework (INFO): Verify sigops are counted in GBT with pre-BIP141 rules before the fork
2017-08-10 06:51:50.062000 TestFramework (INFO): Verify default node can't accept any witness format txs before fork
...

もしくは、test_runner.pyの引数に実行対象のテストファイルを指定する方法もある。

$ test/functional/test_runner.py segwit.py net.py
Temporary test directory at /tmp/bitcoin_test_runner_20170810_155359
............
net.py passed, Duration: 6 s
..................................
segwit.py passed, Duration: 24 s

TEST      | STATUS    | DURATION

net.py    | ✓ Passed  | 6 s
segwit.py | ✓ Passed  | 24 s

ALL       | ✓ Passed  | 30 s (accumulated) 
Runtime: 24 s

全テストケースを実行する場合は、テストファイルを指定せずに、test_runner.pyを実行する。

テストコードに記載されているログは、デフォルトではコンソールには出力されず全てtest_framework.logというログファイルに記載され、テスト実行用の一時フォルダ内に格納される。 コンソールに出力する場合は-lオプションを指定。

またPythonで書かれ、実行されているので、任意の場所に↓を入れて、デバッガをアタッチすることも可能。

import pdb; pdb.set_trace()

テストを追加する場合は、↓を参考にする。

https://github.com/bitcoin/bitcoin/blob/master/test/functional/README.md

utilテストの実行

↓のbitcoin-util-test.pyを実行する。-vはverboseオプションで各テストのログが出力される。

$ test/util/bitcoin-util-test.py -v
2017-08-10 15:45:50,726 - INFO - PASSED: Creates a blank v1 transaction
2017-08-10 15:45:50,731 - INFO - PASSED: Creates a blank v1 transaction (output in json)
2017-08-10 15:45:50,735 - INFO - PASSED: Creates a blank transaction when nothing is piped into bitcoin-tx
...

プルーニングピア向けのservice bitを定義したBIP-159

Bitcoinのmainnetのブロックチェーンのデータサイズは現在150GB弱ほどだが、プルーニングモードで動作させると使用済みのTXOを指定サイズ分だけ保持し、古いTXOデータは削除されていくようになり、とても少ないディスクスペースでフルノードを動作させることができる。

ただ、昔の履歴ブロックは消えてしまうため、IBD(Initial Block Download)などでジェネシスブロックからの古いブロックの要求があってもそれに答えることはできない。そのためプルーニングモードを使用している場合は、nServiceFlagsNODE_NETWORKがアンセットされるようになっている。

ただIBDはできないけど、最近リレーされたブロックであれば提供することができるため、プルーニングノード向けに新たにservice bitを定義して、直近のブロックやヘッダ、トランザクションは他のフルノードと同様リレーできるようにしようということでBIP-159が提案された↓

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

動機

プルーニングモードで動作しているピアは、全ブロックの歴史を提供することはできないが、それ以外は従来のピアと同じサービスを提供できる。Bitcoinは現在、ピアが全ての履歴ブロックを提供できることを示すNODE_NETWORKのservice bitのみを提供している。

  1. プルーニングされているピアは、ブロック、ヘッダ、トランザクション、アドレスをリレーすることができ、履歴ブロックも限られた数であれば提供できるため、プルーニングピアもそのserviceをアナウンスする方法を持つべきである。
  2. 他のピアが非プルーニングピアからブートストラップできるようにするため、IBDが終わったピアは、プルーニングされたピアへのいくつかのアウトバウンド接続を考慮する必要がある。

仕様

新しいservice bits

このBIPでは2つの新しいservice bitsを定義する↓

名称 bit 内容
NODE_NETWORK_LIMITED_LOW bit 10 (0x400) このservice bitが通知された場合、ピアは少なくとも最新の288ブロック(現在のBitcoin Coreの最小値で約2日分のブロック)に対応できなければならない。
NODE_NETWORK_LIMITED_HIGH bit 11 (0x800) このservice bitが通知された場合、ピアは最新の1152ブロック(約8日分)に対応できなければならない。

NODE_NETWORK_LIMITED_*のservice bitをシグナリングしているピアに接続する際は、チェーンの再編成を処理するため144ブロックのセーフティバッファを考慮する必要がある。

アドレスのリレー

このBIPに従うフルノードは、(NODE_NETWORK_LIMITED_*のservice bitをシグナリングしているピアを含む)接続中のピアから(addrメッセージ等で送られる)address/servicesをリレーする必要がある。

ピアのフィンガープリント対策

(ディスク容量など各ピアの構成は違うので)プルーニングしいてるピアがどれだけのブロック数を保持しているかはピアによって異なり、それがフィンガープリンティングの弱点につながるかもしれない(getdataメッセージでピアがプルーニングしている深さを知ることができる)。NODE_NETWORK_LIMITEDをサポートするピアは、プルーニングしている深さを漏らさないようにしなければならない。そのため、通知されたNODE_NETWORK_LIMITED_*閾値(↑よりそれぞれ288ブロックと1152ブロック)より深いブロックを要求するメッセージが来ても処理してはならない。

リスク

このBIPに従うプルーニングピアは、より多くのアウトバウンドの帯域幅を消費する可能性がある。

リレーされたaddrメッセージのnServiceFlags(service bits) をチェックしていない軽量クライアントは、意図せずプルーニングピアに接続し、プルーニング済みのブロックを要求する可能性がある。この場合プルーニングピアは対象のブロックデータを返せないので、そういう事態を避けるために軽量クライアントはservice bitsをチェックしなければならない。DNSシードを介してピアのIPを取得する軽量クライアントは、DNSフィルタリングオプションを使う必要がある。

互換性

この提案は後方互換性がある。

参照実装

github.com

2017年8月8時点ではまだマージはされてない。

所感

  • 接続先のノードがプルーニングノードかどうかは、今までNODE_NETWORKがセットされてないノードという識別方法だったけど、上記のservice bitsが追加されたことで明示的にプルーニングノードが識別できるようになる。
  • プルーニングピアにプルーニングされたブロックを要求した場合、そのピアの挙動としては何も応答を返さない?

Bitcoinの高速リレーネットワークFIBRE

ブロックサイズを拡張について、データ量が多くなるためデータを転送する際により高いネットワーク帯域が求められるようになり、ハッシュパワーの集中が促進するのではないかという懸念がある。 より高速なリレーネットワークを活用することで、この問題を解消できるのではないかということで、Matt CoralloによりFIBRE(Fast Internet Bitcoin Relay Engine)というパブリックに利用可能なBitcoinの高速リレーネットワークが2016年7月に発表されている↓

http://bitcoinfibre.org/

簡単に言うと、TCPだと長距離伝送する際のパケットロスが発生し、パケットの再送が必要になりネットワークの往復によるスパイクが発生するので、FEC(前方誤り訂正)のあるUDPを使って受信側のピアがロスしたパケットについて送信元に再度リクエストするのではなく、受信済みのデータから欠落したデータを再構築できるようにし、余分な往復をさせないようにし、Compact Blockと組み合わせて高速なリレーネットワークを実現しようというもの。

FIBREがどういうコンセプトで作られたのかは、Matt Coralloのブログにまとめられている↓ので見てみる。

The Future of The Bitcoin Relay Network(s) · BlueMatt's Blog

FIBREの設計

オリジナルのリレーネットワークにおいて、ブロックを1つまたは2つのパケットで送信することが出来なかった。この結果、TCPの長距離伝送の処理が不十分になり、リレー時間が著しく急上昇する(元は100ミリ秒〜300ミリ秒だったのが1秒以上に)。FIBREはオリジナルのリレーネットワークの設計を以下の2つの点で改善している。

  • FEC(前方誤り訂正)のあるUDPを使用して、最初に余分なデータを送信することで、パケットロスによるリレー時間のスパイクを排除する。
  • Bitcoin CoreのCompact Blockを使った圧縮

TCPは大量のデータを送信する際に適切な帯域幅で信頼性の高い伝送ができるよう設計されているが、小中規模のデータを確実に低いレイテンシーでリレーするのには向いていない。TCPでは、それぞれ1500バイト以下に調整されたパケットを一度だけ送信し、相手側から応答があって初めて一部のパケットが失われたことが検出できる。パケットロスが発生したことを検知した送信側は、再度ロストしたパケットを相手に再送信し、相手は受け取ったパケットから元の送信を再構築する。この余分な往復の時間は、オリジナルのネットワーク上のリレー時間に大きなスパイクを発生させる。

インターネットにおける長距離伝送では平均して1%のパケットロスが確認された。この場合、余計な往復なく完全な非圧縮のブロックを送信する可能性はだいたい0.991000000/1500 = 0.1%となる。さらにパケットロスはより多くのデータを送信するにつれて増える。(約10パケットで)15KBのデータを送信するだけでも、平均的なホスティングプロバイダでは成功する確率は90%である。往復のレイテンシーが100〜200ミリ秒の1Gbpsもしくは100Mbpsのリンクについて言えば、1回の往復のコストは、送信可能なデータ量から考えると大幅なコスト高になる。

従って、最大限に低遅延なブロック送信を行なうためには、再送の必要性がないようにする必要がある。再送を必要としないようにするためには、途中でパケットが失われても送信側に再度データを尋ねること無く受信ピアがブロック全体を再構築できるだけの充分なデータを送信する必要がある。こういうった分野は、同じ要件を持つビデオ会議のおかげで、よく研究がされている分野になる。一般的な解決策は、失われたパケットの隙間を埋めることができるデータを送信する、比較的単純な線形代数を用いた(=FEC(前方誤り訂正)のある)UDPベースの送信である。

このようにしてパケットロスの問題を解消しても、1Gbpsのリンクで1MBのデータを送信するのは数ミリ秒だが、FECデータの構築と送信によるオーバーヘッドの時間は2倍以上になる。そのためできるだけデータを圧縮するという意味で、Bitcoin CoreのCompact Blockの仕組みはパフォーマンスにとって重要になる。

Compact Blockの設計はFIBREの作業を進めていた時に同時に進められていたので、cmpctblockメッセージのフォーマットはUDP-FECベースのリレーメカニズムに収まるよう設計されている。Compact BlockとFIBREの唯一の違いは、FIBREではデータをFEC付きのUDPで送信し、mempoolに欠落しているトランザクションを要求するために往復するのではなくブロックのトランザクションがすぐにFEC付きで送信する。

FIBREのもう1つの改善点として、サーバに個々のパケットが到着するとすぐにそのパケットをピアにリレーする。残念ながらFIBREのFECエンコーディングの性質上、個々のパケットが正当なブロックの一部かどうかを知ることはできず、同じグループによって実行されているノード間でのみ、この最適化が可能になる。

Bitcoinのリレーとリレーネットワークの集中化

Bitcoinのリレーネットワークの集中化の苦情の多くは、その存在がブロックの検閲する立場にあるという点にある。より効果的なP2Pネットワークであればそういった攻撃の影響を減らすことができるが、完全には解決できない。慎重に選択されたネットワークトポロジーが確実にP2Pネットワークのレイテンシーを上回るため、この問題を唯一解決するには、パブリックなリレーネットワークを追加することである。残念ながらソフトウェアがオープンであるにも関らず、設置された唯一の他のリレーネットワークは個々のマイナーによって運営されるプライベートなものであった。半公式のFalconというリレーネットワークが登場した時、その設計者はスクラッチでスタートし過去数年間のBitcoinのブロックリレーに関する知識を取り入れていないものだった。

FIBREがBitcoinの中継の分散化をさらに促進するためには、FIBREネットワークを設定するのが明らかに簡単である必要がある。そのためFIBREソフトウェアはBitcoin Coreをフォークしアドオンモジュールとして設計されており、Bitcoin Coreを含めた形でリリースされるので、既存のマイナーのシステムにも簡単に組み込むことができる。さらに、ソフトウェアの構成から世界中のレイテンシーを最小限に抑えるためのホスティングプロバイダーの選択方法まで全てを網羅したガイドが以下の記事でまとめられている↓

FIBRE Fast Internet Bitcoin Relay Engine

2017年8月現在は、Bitcoin Core 0.14.1をベースにしたFIBREが公開されている↓

github.com

あまり話題にならないけど、ブロックサイズの拡大が叫ばれる裏ではこういったリレーネットワークやUTXOのサイズ削減など地道なリサーチが続けられているので、その辺も追っていきたい。

HFリサーチ”BIP-MSMMHHF”

Welcome - Bitcoin Hard Fork Research

↑の2つめの、James Hilliardが書いたマルチステージマージマイニングヘッダハードフォーク↓について見てみる。

https://github.com/jameshilliard/bips/blob/bip-msmmhhf/bip-msmmhhf.mediawiki

簡単に言うと、ハードフォークを2段階で実施する提案で、初期フェーズでは新しいチェーンのブロックで、元のチェーンのブロックをマージマイニングする。
その間、元のチェーンでは空ブロックが生成され新しくコインが生成されることもない。一定期間経過したら、元のチェーンのマージマイニングをやめ、そのタイミングで元のチェーンとマージマイニングするために使っていたヘッダフォーマットも変更するという内容。

動機

プールマイニングにおいては、ヘッダフォーマットを大幅に変更することでしか修正できないブロック保留攻撃*1などの深刻な問題が存在する。

また他にも非マージマイニングと互換性のある方法でのみ可能な、多くの望ましいヘッダフォーマットの変更がある。

元のチェーンを永久に無効にできない場合、2つの実行可能なチェーンが存在するリスクが高くなる。

仕様

直近の4032ブロックのうち3900ブロック(約96.7%)にバージョンフラグがセットされていた場合、このフォークをアクティベートする。このフラグは以下の2つのステージ(マージマインステージ、ヘッダ変更ステージ)を同時にロックインする。

マージマインステージ

最初のハードフォークはマージマイニングを使用して実装する。元のチェーンではトランザクションを一切含まないことに加えて、新しいコインが生成されない。
さらに元のチェーン上でntimeを操作し意図的に難易度を上げ、アップグレードしていない全クライアントが現在の時刻に追い付かないようにするコンセンサスルールを追加する。意図的なntimeの操作は、新しいチェーンのブロックのコンセンサスルールとして実装される。

新しいヘッダ
new_header.prev = 前のブロックヘッダのハッシュ
new_header.small_nonce = 4バイトのnonce
new_header.big_nonce = 8バイトのnonce
new_header.... (新しいフィールドを追加可能)
フェイクブロック

元のチェーンに対して↓のフェイクブロックを作る。

block.version = 4
block.prev = new_header.prev
block.merkle = コインベースでマークルルートを計算(コインベースのみで他のトランザクションを含まない)
block.timestamp = 前のブロックのmedian time past + 1
block.bits = calculate_bits()
block.nonce = new_header.small_nonce
block.tx_count = 1

(過去11ブロックのタイムスタンプの中央値 + 1秒した時間が次のブロックのタイムスタンプになる)

コインベース

フェイクブロックのコインベースは↓

coinbase.version = 1
coinbase.tx_in_count = 0
coinbase.tx_out_count = 1
coinbase.tx_out[0].value = 0
coinbase.tx_out[0].pk_script = "OP_RETURN"

(コインの報酬を受け取るコインベースのコインの値が0なので、新しいコインは生成されていないことになる。)

これはメインチェーンを破壊する最後の手段となる攻撃である。median time pastは非常にゆっくりと増加する(6ブロックに1秒)。これはマイニングの難易度の更新タイミングであるretarget period(2016ブロック)の間に336秒しかブロックのタイムスタンが増えないことを意味し、こうなるとretarget peroid毎に難易度は(調整の上限である)4倍に上昇することになる。

Bitcoinのチェーンが一定の難易度にとどまっていると、4倍時間がかかるようになる。

2週間後: 4倍の難易度   (1期間につき2週間)
10週間後: 16倍の難易度 (1期間につき8週間)
42週間後: 256倍の難易度 (1期間につき32週間)

ヘッダ変更ステージ

これはハードフォークの最終段階で、ヘッダーフォーマットがマージマイニングと互換性がなくなる。マージマインステージから42,336ブロック後で、元のチェーンのretarget periodの境界ブロックがスタートしたタイミングでアクティベートされる。アクティベーションの条件は、タイムワープ後元のチェーンが6048ブロック経過したタイミング。

論拠

この解決策は、1つのロックイン期間を使った2段階のハードフォークをする。

最初の段階では、難易度の調整をするための2016ブロックのマイニングが非常に困難になるまで元のチェーンのネットワークの難易度を意図的に増加させるためntimeの増加を抑制し、元のチェーンを実質使えくするよう設計されている。これによりハードフォークに対応していないクライアントにとってハードフォークへの対応が必要なことが明らかになる。

2つのステージが同時にロックインすることで、クライアントのマージマイニングもヘッダ変更ステージでロックされ、ヘッダ変更ステージを迎えると元のチェーンも停止することを保証する。1年以上のマージマイニングを経て元のチェーンの難易度を大幅に上昇させ、現時点の時刻まで追いつくよう難易度を減少させるにはとてつもないコストがかかるようになる。

後方互換

このハードフォークが実施されると、ハードフォークをサポートしていないフルノード及び軽量ノードはどちらも無効になる。移行には全ノードのアップグレードが必要で、マイナーは圧倒的大多数のサポートを表明する必要がある。

所感

  • マージマイニングで、元のチェーンのタイムスタンプに細工して意図的に元のチェーンの難易度を上昇させていくアプローチ面白い。
  • 厳密には元チェーンでのマイニングの結果コインを生成する権利を得てるんだけど、コインベーストランザクションにOP_RETUNの出力で実質そのコイン捨ててる。
  • HFのためにマージマイニングを採用する提案で内容としてはシンプル。他の拡張については特に触れていない。

*1:プールの運営者に経済的な損失を与える攻撃で、マイナーがマイニングに成功して有効なハッシュを見つけてもプールに報告しないという攻撃

HFリサーチ”BIP-MMHF”

ブロックサイズの拡張(1M→2M)を目的としたハードフォークに注目が集まっているけど、ブロックサイズとは別に↓のような問題を解消するためにブロックヘッダの構造についてもそろそろ変更が必要じゃないかと思う。

  • extra nonceの確保
    現在のハッシュレートではとても既存のnonce領域では足りず、タイムスタンプやコインベースの値がextra nonceとして使われている。
  • segwitのコミットメント領域の確保
    BIP-141のsegwitはソフトフォークとして提案されており、segwitの署名を含むトランザクションのコミットメントはコインベーストランザクションの出力の1つにOP_RETUNRを使って追加する仕様になっている。もともとOP_RETUNRの導入やサイズ拡張については賛否あったのを考えると、それをコアとなる機能の一部として採用するのは皮肉な結果にも思える。本来は従来のマークルルートと同様、ブロックヘッダに専用の領域として定義すべきものだと思う。

ただ、このようなブロックヘッダの構造に変更を加える場合は当然ながらハードフォークが発生する。
ハードフォークが行われるとチェーンの分岐が考えられ、チェーンの分岐を技術的に防ぐことは現状不可能で、社会的なコンセンサスの形成が必要になる。

というのを考慮するとハードフォークは気軽に行えないので、個人的にはサイズ拡張だけでなく↑のようなブロックヘッダの拡張も合わせて計画し、実施した方が現実的なんじゃないかと思う。(まぁ何を加える加えないで政治的対立がまた発生しそうな気もする)

ではどういう拡張が考えられるのか、ハードフォークに関するリサーチが↓のサイトにまとめられている。

Welcome - Bitcoin Hard Fork Research

今回は1つめの、ブロックサイズとnonceのスペースを拡張し、Native merge-miningをサポートするLuke-jrによる提案(2016年2月に提案されたけどまだBIPにはなってない)↓について見てみる。(残念ながらマージマイニングのサポート方法についてはまだ詳しく記載されていない)

https://github.com/luke-jr/bips/blob/bip-mmhf/bip-mmhf.mediawiki

動機

Bitcoinのマイニングは、有効なブロックを見つけるためにデータを変更してハッシュ値を計算する必要がある。32bitのnonceはブロックヘッダに直接設定する。このnonceのスペースを使い尽くしても目的のハッシュが見つからないと、マイナーはトランザクションデータを変更しなければならない。その場合、コインベーストランザクションのscriptSigを変更することで、マークルルートの値が別の値に変わり、新しいマークルルートで再びnonceを変えながらハッシュ計算を続ける。しかしこのプロセスは複雑だしハードウェアにアウトソースするのに適していないが、現在のハッシュレートではそうせざるを得ない。この問題についてはハードウェアにアウトソースできるようnonceのスペースを拡張することで解決できる。

※他のブロックサイズの制限や、マージマイニングについての動機はまだ書かれていない

仕様

ブロックヘッダのフィールド

各ブロックは↓のヘッダフィールドで構成されるようになる。

  • 最大600bitのnonceフィールド
    • PoW計算の最終段階で直前に変更可能な32bitのclass 1 nonce。
    • 24bitのclass 2 nonce
    • 32bit〜544bitの可変長のclass 3 nonce
  • 前のブロックのハッシュ
  • 32bitのタイムスタンプ
  • ブロック高
  • 8bitのmerge-mining-hardforkのデプロイのビットフィールド
  • 16bitのハードフォークのデプロイのビットフィールド
  • 32bitのソフトフォークのデプロイに使用するビットフィールド(現在のBIP-9のようなもの)

これに加えてブロックにはトランザクションのセットが含まれる。

トランザクションは前のブロックからUTXO stateを更新する必要がある。UTXO stateに存在しないデータを使用しようとするトランザクションは無効となる。

有効なUTXOを使用する入力のwitnessは、その入力が参照するpubkey scriptに対して有効なものかチェックする。このチェックに失敗すると、そのブロックは無効である。

ブロック内のトランザクションの総コストはXXXを超えてはならず、またsigopsの合計がXXXを超えてはならない。 ( XXXの値はまだ決まってないみたい。)

マークルツリーのアルゴリズム

マークルツリーの構成にも変更が加わる。各オブジェクトについて、マークルツリーでそのダイジェスト値を計算する。各ダイジェストはハッシュとそのノードに含まれる値の合計値で構成される。

例えば、トランザクションのマークルツリーのダイジェストは↓の3つのフィールドで構成されるようになる。

uint256 hash;  // SHA-256ハッシュ
uint64 fees;   // このノード以下のトランザクションで支払われる総手数料
uint64 size;   // このノード以下のトランザクションの合計サイズ

witnessのマークルツリーのダイジェストは↓の3つのフィールドで構成される。

uint256 hash;  // SHA-256ハッシュ
uint32 sigops; // このノード以下のトランザクションのsigopsの合計値
uint64 size;   // このノード以下のトランザクションの合計サイズ

以下の3つのbitがflagsバイトとして定義される。

FLAG_LEAF = 0x80
FLAG_PLURAL = 0x40
FLAG_MERKLE_ROOT = 0x20

残りのbitは予約されており、現在は全て0。

  • FLAG_LEAFはツリーの最下位レベルにのみセットする。
  • FLAG_PLURALは2つのハッシュを1つに圧縮する際にセットする。
  • FLAG_MERKLE_ROOTはマークルルートを計算するための最終的なハッシュステップにセットする。

トランザクションデータとwitnessのダイジェストは↓のように生成する。

トランザクションツリーを生成する際は、最初に↓のようにリーフダイジェストを生成する。

 uint256 hash = txid
unit64 fees = 入力のコインの合計 - 出力のコインの合計
uint64 size = witnessデータを除外してシリアライズしたトランザクションのサイズ

witnessツリーを生成する際は、最初に↓のようにリーフダイジェストを生成する。

uint256 hash = txid
uint32 sigops = トランザクションのsigop数
uint64 size = witnessデータを含むBIP-144でシリアライズしたトランザクションのサイズ

各ダイジェストのペア(左と右)から、その親ノードを↓で計算する。

flags = FLAG_PLURAL (or FLAG_PLURAL | FLAG_LEAF for the first pass)
    
compressed.hash = SHA256d(left.hash | left.fees | left.size | right.hash | right.fees | right.size | flags)
compressed.fees = left.fees + right.fees
compressed.size = left.size + right.size

ノードの数が奇数の場合、↓のようにして親ノードを計算する。(今までと同じように左ノードのデータをコピーして使うと手数料やデータサイズの計算がおかしくなるため、ゼロ値を使ってると思われる)

flags = 0 (or FLAG_LEAF for the first pass)
    
compressed.hash = SHA256d(left.hash | left.fees | left.size | 256 zeros | 64 zeros | 64 zeros | flags)
compressed.fees = left.fees
compressed.size = left.size

このダイジェストの数が1つになる(ルートノードになる)まで計算ステップを繰り返す。

最後にマークルツリーのルートを作成するためルートハッシュを計算する。(今までのようにルートノードハッシュがそのままマークルルートになる訳ではない)

flags = FLAG_MERKLE (or FLAG_MERKLE | FLAG_LEAF)
    
final_root_hash = SHA256d(root.hash | root.fees | root.size | element_count | 192 zeros | 64 zeros | 64 zeros | flags)

element_countは、8バイトの数値で、このツリーのリーフノード(トランザクション)の数

witnessツリーの場合は、合計部分は含まれずflagsは同じで計算はシンプル。手数料の部分をsigop数に置き換えれば良い。

動的メンバーシップマルチパーティ署名(DDMS)アルゴリズム

ハッシュTMRトランザクションマークルルート)は、ブロックに含まれる全トランザクションのidを使ったマークルツリーアルゴリズムを実行することで生成される。

ハッシュWMR(Witnessマークルルート)は、ブロックに含まれる全トランザクションの(witnessデータを含む)完全なハッシュを使用したマークルツリーアルゴリズムを実行することで生成される。

ハッシュH-C(Header-C)は、↓のデータをSHA256して生成される(エンディアンについては未FIX)。

ハッシュCMR(コミットメントマークルルート)は、最大232個の要素を持つハッシュH-Cのマークルツリーアルゴリズムを実行することで生成されるが、ハッシュH-Cにつながる部分木としてのみ検査される。ノードはマークルツリーの他のブランチに関して何も気にせず、H-Cの位置は以下のように計算する。

uint32_t lrot(uint32_t x, uint32_t n) {
  return (x << n) | (x >> (32 - n));
}
// nonceはclass 3 nonceの先頭4バイトでビッグエンディアンとして解釈される。
uint32_t vector_position_for_hc(uint32_t nonce, uint32_t vector_size) {
  const uint32_t chain_id = 0x62697463;  // "bitc"
  uint32_t a, b, c;
  a = (0xb14c0121 ^ chain_id) - lrot(chain_id, 14);
  b = (nonce ^ a) - lrot(a, 11);
  c = (chain_id ^ b) - lrot(b, 25);
  a = (a ^ c) - lrot(c, 16);
  b = (b ^ a) - lrot(a, 4);
  c = (c ^ b) - lrot(b, 14);
  a = (a ^ c) - lrot(c, 24);
  return a % vector_size;
}

ハッシュH-B(Header-B)は、↓のデータをSHA256して生成される。

  • 41バイトの定数:
77 77 77 77  01 00 00 00  00 00 00 00  00 00 00 00
00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00
00 00 00 00  00 ff ff ff  ff
  • 1バイト:以下の41〜103バイトのデータ長 - 3
  • 41〜103バイト:
    • シリアライズしたブロック高(BIP-34)
    • 1バイト:マージマイニングハードフォークのデプロイ用のビットフィールド
    • ハッシュCMR
    • class 3 nonce
  • 1バイト:前のデータ長(midstate圧縮用)
  • 14バイトの定数:
01 00 00 00  00 00 00 00  00 00 00 00 00 00

ハッシュH-A(Header-A)は、↓のデータをSHA256して生成される。

  • 3バイト:class 2 nonce
  • 1バイト:定数0x60
  • 32バイト:前のブロックのハッシュ
  • 32バイト:ハッシュH-B
  • 4バイト:タイムスタンプ
  • 4バイト:現在のブロックのターゲット(difficulty)
  • 4バイト:class 1 nonce

ハッシュH-Aはリトルエンディアンの数値として解釈され、現在のブロックターゲットと比較される。ハッシュ値がターゲット以下であればDDMS署名は有効。

ハードフォークのデプロイ用ビットフィールド

↓のハードフォークの性質上、ハードフォークのデプロイ用ビットフィールドはBIP-9のように機能する。

  • ノードは、ハードフォークに関してネットワーク全体の同意が示されるまで、starttimeを延期しなければならない。
  • マイニングノードは、ネットワーク全体がハードフォークに合意し、ノードのサポートをを確信するまでstarttimeを延期しなければならない。
  • 認識していないハードフォークがネットワークの最長のチェーンでアクティベートされた場合、ノードのオペレーターがハードフォークを受け入れるか、拒否するか選択するまでノードは旧ルールの最後のブロックで停止する必要がある。
  • 拒否されたハードフォークのビットがアクティベートされると、ノードはフォークポイントより前進するため代わりのPoWアルゴリズムに切り替える必要がある。
  • ハードフォークはそれが認識/実装されるまでノードはそれを受け入れない。ノードがアップグレードされるまで、ノードは認識していない、不参加状態になる。
  • シグナリング期間中XXX%以上がハードフォークを拒否する場合、ハードフォークはアクティベートしてはならない。

論拠

nonceのサイズ制限の1つの解決策は、ブロックヘッダ内のnonceのスペースを拡張することで、現在80バイトに固定されているブロックヘッダの長さを可変にするか、未使用のセクションを追加のnonceフィールドとして再利用できるようにするかのどちらかになる。ブロックサイズを拡張するのは、単純により多くのトランザクションを許容するようにするだけで充分。

しかし、こういったハードフォークは、旧ノードを壊すだけでなく、新しいブロックの受け入れを拒否し、新しいチェーンより短くても古いルールを守るチェーンを有効なチェーンとして受け入れることができてしまう。これはこのハードフォークを実装する際に、旧ノードが有効な空ブロックとして認識するハッシュを使用することで対処することができる。

後方互換

このハードフォークは、明示的にこのハードフォークに対応していないノードでは(フルノードも軽量ノードも)無効になる。移行するには、全ノードがアップグレードを選択する必要があり、マイナーは圧倒的大多数がサポートを表明する必要がある。

まとめ&所感

  • nonceは現在32bitなので、600bitになると19倍弱増えることになる。
  • BIP-9のようなソフトフォークのシグナリングを今はブロックのnVersionをビット列として解釈して使用しているが、ソフトフォーク、ハードフォーク、マージマイニング用の各デプロイ用、シグナリング専用のヘッダフィールドが設けられる。
  • マークルツリーを構成するアイテムも変わる。従来リーフノードにTXIDをセットし、2つのノードのハッシュを連結して親ノードのハッシュを計算し、それをルートノードまで繰り返すことでツリーを構成していたけど、そのノードにハッシュだけではなく、用途に応じた値(手数料やsigop数など)がセットされるようになる。
  • 複数のアイテムを組み合わせて↑のようなマークルツリーのアルゴリズム考えるのも面白そう。
  • nonceが3つのパートで構成されるよう拡張され、そのハッシュの計算方法も新たに定義されるが、結構複雑になってる感がある。
  • 旧ノードにとって空のブロックとして認識させる仕組みの部分が詳しく知りたい。