Develop with pleasure!

福岡でCloudとかBlockchainとか。

Bitcoinの次のソフトフォークのデプロイ方法は?

Bitcoinへのソフトフォークは紆余曲折あった2017年8月のSegwitの導入以降、3年経とうとしているが新たに行われていない。次のソフトフォーク候補として有力なのがSchnorr署名およびTaprootの導入だが、BIP化はされたもののまだBitcoin Coreに実装はマージされていないし、具体的なデプロイ方法も決められていない。

Segwitのデプロイ方法は、OP_CSV導入時と同様BIP-9↓が採用された。ただ、このデプロイ方法はマイナーによる投票という捉え方がされSegwit導入には紆余曲折あった。

techmedia-think.hatenablog.com

その後、デプロイのプロトコルに問題があるとして、デプロイ期間の最終日が訪れると自動的にアクティベートされるよう仕様を変更したBIP-8が提案された↓

techmedia-think.hatenablog.com

ただし、BIP-8の仕組みではソフトウェアが対応しデプロイすれば(最終期限日を過ぎると)無条件にアクティベートできるという仕様は、開発者がルールを決めるという印象が強くなる懸念が開発者の中でも挙がった。そして、Segwit導入後も次のソフトフォーク候補は上がるものの(MASTとか)、未だそれがマージされる=新しいソフトフォークデプロイのトリガーを引く状況には至っていない。

そんな中、最近↑のBIP-8への変更の提案がマージされた↓(実はこのBIP-8、2020年2月にBIPのステータスがRejectedになってたが、今回の変更で再度Draftに戻った)

github.com

主な変更点は、

  • これまで、ソフトフォークが指定したタイムアウトまでにアクティベートされなかった場合、無条件にアクティベートされていたが、その振る舞いがlockinontimeoutというブール値のフラグで制御されるようになった。これがtrueに設定されると今までと同様の挙動で、falseに設定されているとタイムアウトするとアクティベートはされず、デプロイ失敗となる。
  • lockinontimeoutフラグの設定は、デプロイの初期段階で設定する必要はなく後からtrueに設定できる。ただし、一度設定するとクリアはできない。

ただ、lockinontimeoutのフラグは、

lockinontimeout should be set to true for any softfork that is expected or found to have political opposition from a non-negligible percent of miners. (It can be set after the initial deployment, but cannot be cleared once set.) 無視できない割合のマイナーからの政治的反対が予想されるもしくは発見されたソフトフォークに対してはtrueに設定しなければならない。

とあるように、開発者がルールを決めるという印象が強くなる懸念は残るように思える。

尚、BIP-8の実装はまだBitcoin Coreに導入されておらず、現在Bitcoin Coreがサポートしているデプロイ方法はBIP-9のみ。実際、次期ソフトフォーク候補のSchnorrやTaprootのデプロイ方法はどうなるのか?個人的にはBCHとの分離などもあり、今回はBIP-9のデプロイでもアクティベートされるんじゃないかと思うけど、どうかなー。

以下、更新されたBIP 8の意訳↓

概要

このドキュメントでは認識されている多数の間違いを修正するBIP-9の代替案を規定している。開始とタイムアウトにはPOSIXタイムスタンプではなくブロック高が使用される。さらに(ソフトフォークと呼ばれる)後方互換性のある変更のアクティベートを保証する追加のアクティベートパラメータが導入されている。

動機

BIP-9ではブロックのnVersionフィールドを再利用して、ソフトフォークの並行デプロイを行うための仕組みが導入された。アクティベーションの際は95%というハッシュレートのほぼ全ての通知が必要になっているが、これは非実用的で、少数のハッシュレートさえあればソフトフォークを拒否することができる。過半数のハッシュレートをベースにしたアクティベーショントリガーであれば、フルノードのアップグレードに代わって過半数のハッシュパワーが新しいルールを適用することでアクティベーションが可能。全てのコンセンサスルールはフルノードによって最終的に強制されるため、最終的に新しいソフトフォークがエコノミーによって強制される。この提案は、これら2つの側面を組み合わせて合理的な期間後にフラグデイによるアクティベーションを提供すると共に、フラグデイ以前でも大部分のハッシュレートによるアクティベーションを有効にする。

ブロック高ではなくタイムスタンプを使っているため、ハッシュレートが突然減少するとアクティベートが遅れる可能性がある。

ブロック時間についてはいささか信頼性が低く、意図的にもしくは意図せず不正確な時間になる可能性があるため、ブロック時間に基づく閾値は理想的ではない。第二に、BIP-9では指定した時間後の最初のretarget periodに基づいてトリガーがを指定しているが、これは直感的ではない。新しいブロックはそれぞれ1つずつ高さが増えていくので、ブロック高に基づく閾値の方がはるかに信頼性が高く直感的で、難しいretargetを正確に計算することができる。

仕様

パラメータ

各ソフトフォークのデプロイは、以下のチェーン毎のパラメータによって指定される(以下で詳しく説明)。

  1. nameは、ソフトフォークの非常に簡単な説明を示し、識別子としての使用に適している。単一のBIPで説明されているBIPの場合、「bipN」という名前の使用を推奨する。Nは適切なBIP番号。
  2. bitは、ブロックのnVersionフィールドのどのビットを使って、ソフトフォークのロックインおよびアクティベーションを通知するか決定するもので、{0, 1, 2, ..., 28}の中から選択される。
  3. startheightは、bitが意味を持つ最初のブロックの高さを指定する。
  4. timeoutheightは、マイナーの通知が終了するブロックの高さを指定する。この高さになった後、(このブロックのbitの状態を除いて)まだソフトフォークがロックインされていない場合、デプロイは全ての後続のブロックで失敗したとみなされる(ただし、FAILING状態の例外を参照)か、lockinontimeoutがtrueの場合はロックイン状態に遷移する。
  5. lockinontimeoutのブール値にtrueがセットされた場合、timeoutheightで状態がロックインもしくはアクティブでない場合、状態をロックインに遷移する。

ガイドラインの選択

ソフトフォークでこれらのパラメータを選択するために以下のガイドラインが推奨されている:

  1. nameは、2つのソフトフォークが並行もしくは別の方法で同じ名前を使用しないように選択する必要がある。
  2. bitは、2つのソフトフォークが同時に同じビットを使用することがないよう選択する必要がある。
  3. startheightは、ソフトフォークの実装を含むソフトウェアのリリース日から約30日後など、未来のブロック高を設定する必要がある。これによりリリース前のソフトウェアを実行している参加者の結果としてのトリガーを防ぎながら、ある程度のリリースの遅れを許容し、アクティベーション前に妥当な数のフルノードがアップグレードされることを保証する。単純にするため、retarget periodの開始となるブロック高に切り上げする必要がある。
  4. timeoutheightは、1年もしくはstartheightから52416ブロック(26 retargetインターバル分)にする必要がある。
  5. lockinontimeoutは、無視できない割合のマイナーからの政治的反対が予想されるもしくは発見されたソフトフォークに対してはtrueに設定しなければならない。(これは最初のデプロイ後に設定できるが、一度設定するとクリアできない)

startheightが前のソフトフォームのtimeoutheightもしくはアクティベーション後であれば同じビットを使ってデプロイすることは可能だが、必要となるまでは推奨しない。またバグのあるソフトウェアを検出するために、一時的に使用しないことを推奨する。

状態

各ブロックとソフトフォークで、デプロイの状態を関連付ける。可能な状態は以下の通り。

  1. DEFINEDは、各ソフトフォークが最初に始まる際の初期状態。ジェネシスブロックは定義上、各デプロイのこの状態にある。
  2. STARTEDは、startheightを超えたブロックに対する状態。
  3. LOCKED_INは、nVersionにセットされた関連ビットが閾値を満たしたSTARTEDブロックの最初のretarget periodの次のretarget periodの状態、もしくはlockinontimeoutがtrueに設定されタイムアウトとなった後の最初のretarget periodにおける状態。
  4. ACTIVEは、LOCKED_IN retarget peroidの後の全ブロックの状態。
  5. FAILINGは、LOCKED_INに達せずlockinontimeoutがfalseの場合に、タイムアウト後の最初のretarget periodの状態。
  6. FAILEDは、FAILING retarget periodの後の全ブロックの状態。

ビットフラグ

ブロックヘッダーのnVersionフィールドは、32ビットのリトルエンディアン整数として解釈され、ビットはこの整数内の値(1 << N)として選択される。Nはビット番号。

STARTED状態のブロックは、位置ビットのビットが1に設定されたnVersionを取得する。そのようなブロックの上位3ビットは001でなければならないため、実際に可能なnVersionの値の範囲は[0x20000000...0x3FFFFFFF]となる。

BIP 34, BIP 66, BIP 65によって課せられた制約により、利用可能なnVersionの値は、0x7FFFFFFBとなる。これにより同時展開可能なソフトフォークは最大30個に制限される。上位3ビットを001に制限することで、本提案ではその内の29個を得ることができ、将来異なる仕組みの2つのアップグレード(上位ビットが010と011)をサポートすることができる。ブロックのnVersionの先頭ビットが001でない場合、デプロイとしては全てのビットが0として扱われる。

マイナーはLOCKED_INフェーズのビットの設定を継続し、ソフトフォークの取り込みが認められ表示される必要がある。該当するビットが設定されていないブロックは、この期間中無効となる。柔軟性のため、このルールでは上位3ビットを特定の方法で設定する必要はない。

新しいコンセンサスルール

各ソフトフォークの新しいコンセンサスルールはACTIVE状態の各ブロックに適用される。

状態遷移

https://github.com/bitcoin/bips/raw/master/bip-0008/states.png

STARTED状態の間、lockinontimeoutがtrueに設定されている場合、timeoutheightに達すると状態がLOCKED_INに遷移する。

ジェネシスブロックは定義により、各デプロイに対してDEFINED状態を持つ。

State GetStateForBlock(block) {
        if (block.height == 0) {
                return DEFINED;
        }

1つのretarget period中のブロックは全て同じ状態となる。floor(block1.height / 2016) = floor(block2.height / 2016)の場合、デプロイ毎に同じ状態になることが保証される。

        if ((block.height % 2016) != 0) {
            return GetStateForBlock(block.parent);
        }

それ以外の場合、次の状態は前の状態によって異なる。

        switch (GetStateForBlock(GetAncestorAtHeight(block, block.height - 2016))) {

開始ブロック高になるまでは、初期状態のまま。

        case DEFINED:
            if (block.height >= startheight) {
                return STARTED;
            }
            return DEFINED;

STARTED状態の期間の後は、設定されたビットを集計し、過去の期間の十分な数のブロックがバージョン番号内にデプロイのビットを設定した場合はLOCKED_INに遷移する。閾値は1916ブロック以上(2016の95%)、testnetでは1512ブロック以上(2016の75%)。閾値に達しておらずタイムアウトを迎えた場合は、lockinontimeoutがtrueの場合LOCKED_INに遷移し、そうでなければFAILINGに遷移するかのどちらかだ。

ブロックの状態が自身のnVersionに依存することはなく、祖先のブロックのnVersionにのみ依存する。

        case STARTED:
            int count = 0;
            walk = block.parent;
            for (i = 0; i < 2016; i++) {
                walk = walk.parent;
                if (walk.nVersion & 0xE0000000 == 0x20000000 && (walk.nVersion >> bit) & 1 == 1) {
                    count++;
                }
            }
            if (count >= threshold) {
                return LOCKED_IN;
            } else if (block.height >= timeoutheight) {
                return (lockinontimeout == true) ? LOCKED_IN : FAILING;
            }
            return STARTED;

タイムアウト(またはlockinontimeout)によりLOCKED_INになっていない場合、全てのブロックの満場一致の通知により、デプロイがアクティブになる可能性がある1つのretarget periodがある。この状態はlockinontimeoutが後にtrueに設定された場合に、元のデプロイとの互換性を維持するために存在する。

        case FAILING:
            walk = block;
            for (i = 0; i < 2016; i++) {
                walk = walk.parent;
                if (walk.nVersion & 0xE0000000 == 0x20000000 && ((walk.nVersion >> bit) & 1) != 1) {
                    return FAILED;
                }
            }
            return ACTIVE;

LOCKED_INのretarge periodの後は、自動的にACTIVEに遷移する。

        case LOCKED_IN:
            return ACTIVE;

またACTIVEとFAILEDは最終状態で、そこに到達するとデプロイの状態はそのままになる。

        case ACTIVE:
            return ACTIVE;
        case FAILED:
            return FAILED;
        }
}

実装

状態はブロックチェーンの分岐に沿って維持されるが、再編成が発生した際に再計算が必要になる場合があることに注意すること。

特定のブロック/デプロイメントの組み合わせの状態は、現在のretarget period前の祖先(つなりblock.height - 1 - (block.height % 2016)を持つ祖先まで)によって完全に決定される。親によってインデックスされた2016の倍数のブロックの結果の状態をキャッシュすることで、上記の仕組みを効率的かつ安全に実装できる。

警告の仕組み

アップグレードの警告をサポートするため、暗黙のビットmask = (block.nVersion & ~期待するバージョン) != 0を使って、未知のアップグレードが追跡できる。nVersionに予期しないビットが設定されると、maskは0以外の値になる。未知のアップグレードに対するLOCKED_INが検出される度に、ソフトフォークが行われることを大きな警告をする必要がある。次のretarget periodになると(未知のアップグレードがACTIVE状態の場合)、さらに大きな警告をする必要がある。

getblocktemplateの変更

template requestオブジェクトが新しいアイテムを含むよう拡張される。

template request

キー 必須 タイプ 定義
rules No Stringの配列 nameを使ったサポートするソフトフォークのデプロイのリスト

templateオブジェクトも拡張される。

template

キー 必須 タイプ 定義
rules Yes Stringの配列 nameを使ったアクティブ状態のサポートするソフトフォークのデプロイのリスト
vbavailable Yes オブジェクト サポートするソフトフォークのデプロイの保留中のセット。それぞれがキーとしてnameを使い値としてソフトフォークビットを使う。
vbrequired No 数値 送信でサーバーが有効にする必要があるソフトフォークのデプロイビットのビットマスク

テンプレートの「version」キーは保持され、サーバーのデプロイ設定を示すのに使用される。versionbitsが使用されている場合、「version」は必ず[0x20000000...0x3FFFFFFF]のversionbitsの範囲内である必要がある。マイナーは特別な「mutable」キーなしで、ブロックのバージョンをクリアもしくは設定することができるが、それらがテンプレートの「vbavailable」にリストされているか、(クリアの場合は)「vbavailable」にリストされていない必要がある。サーバーは生成されたブロックが有効であることを確認するために、LOCKED_INのデプロイに対して「vbrequired」ビットを設定しなければならない。

「rules」もしくは「vbavailable」のキーとしてリストされているソフトフォークのデプロイ名の前に「!」を付与することができ、このプレフィックスが無い場合、GBTクライアントはルールがテンプレートの使用に影響を与えないことを想定している可能性がある。この典型的な例は、BIP 16,65, 66, 68, 112, 113のように以前は有効だったトランザクションが有効でなくなった場合。クライアントがプレフィックス無しのルールを理解しない場合、マイニングにそれらを変更せずに使用する可能性がある。一方、このプレフィックスが使われている場合、ブロックの構造や生成するトランザクションに対する変更を示す。この例は、(コインベースの構成を変更した)BIP-34や(tixdのハッシュの変更および生成するトランザクションへのコミットを追加した)BIP 141だ。「!」のプレフィックスルールを理解しないクライアントは、テンプレートを処理してはならないし、変更されていない場合もそれをマイニングに使ってはならない。

参照実装

https://github.com/bitcoin/bitcoin/compare/master...luke-jr:bip8

BIP 9との比較

  • lockinontimeoutフラグが追加された。BIP 9ではタイムアウトに達するとFAILED状態に遷移するだけ。
  • デプロイのクロックとして、median-time-pastではなくブロック高が使用される。
  • 新しく最後の努力が行えるFAILING状態が追加され、初期デプロイ後にlockinontimeoutが安全に設定できるようになった。

後方互換

BIP 8およびBIP 9によるデプロイでは、有効なデプロイビットを共有してはならない。BIP 9のみを実装しているノードは、ハッシュパワーの閾値がtimeoutheightまでに到達していない場合、BIP 8ソフトフォークをアクティベートしないが、アクティベートしたノードが生成したブロックは受け入れる。

デプロイ

BIP 8を使ったデプロイ提案の現在のリストはこちら