Develop with pleasure!

福岡でCloudとかBlockchainとか。

複数のソフトフォークの並行デプロイを可能にするversion bits(BIP-9)

先日Bitcoin Coreの0.12.1がリリースされた。

Bitcoin Core :: Bitcoin Core 0.12.1 Released!

このリリースでBIP-68、BIP-112、BIP-113のソフトフォークがリリースされることになったが、このソフトフォークのリリースで初めてversion bitsを利用したデプロイが実施されたみたい。

version bitsというのは↓のBIP-9で提案された仕組みで、Bitcoinのブロックのversionフィールドの解釈を変更し、複数の下位互換性の変更(ソフトフォーク)を並行してデプロイする方法を提案している。versionフィールドをビットベクトルとして解釈することで、各ビットを個別の変更を追跡するために使う。コンセンサスの変更が成功するかタイムアウトすると、休止期間を経て後の変更の追跡に再利用される。

bips/bip-0009.mediawiki at master · bitcoin/bips · GitHub

仕様

各ソフトフォークのデプロイは、以下のパラメータで指定される。

  1. nameには、識別子として使用するのに適切なソフトフォークの名称を指定する。ソフトフォークが単一のBIPで記述されている場合は、 “bipN"のようにBIP番号を指定するのを推奨する。
  2. bitには、ブロックのnVersionフィールドのどのビットがソフトフォークのアクティベーション結果を知らせるのに使用されるビットか指定する。ビットは{0,1,2,…,28}のセットの中から選択される。
  3. starttimeにはそのビットが有効になるブロックの最小のmedian time-past*1を指定する。
  4. timeout にはデプロイが失敗したとみなす時刻を指定する。ブロックのmedian time pastがtimeoutの時刻以上でソフトフォークがまだロックイン状態にない場合、デプロイは失敗したとみなされる。

パラメータ選択のガイドライン

ソフトフォークのデプロイの際に↑のパラメータを選択する際のガイドラインが↓

  1. bitを選ぶ際は、同時にリリースする2つのソフトフォークで同じビットを選択しないこと。
  2. starttime に指定するのは未来日で、ソフトフォーク含むソフトウェアのリリース日のだいたい1ヶ月後を指定する。
  3. timeout にはstarttimeの1年後(31536000秒後)を指定する。

後のソフトフォークのデプロイの際に、前のデプロイのタイムアウトアクティベーションの期間より後のstarttimeを設定しさえすれば前のデプロイと同じビットを使用することは可能だが推奨しない。

状態

各ブロックとソフトフォークには以下のようなデプロイの状態が関連付けられる。

  1. DEFINED
    各ソフトフォークをリリースした最初の状態。
  2. STARTED
    ブロックがstarttimeを経過した状態。
  3. LOCKED_IN
    STARTEDブロックの最初のretarget期間より後で、nVersionに閾値以上のビットが関連付けられた状態。
  4. ACTIVE
    全てのブロックがLOCKED_INのretarget期間後になる状態。
  5. FAILED
    LOCKED_INに達せず、retarget期間がタイムアウト期間を過ぎた状態。

Bit flags

STARTED状態にあるブロックは、1がセットされているビットのnVersionを取得する。そのようなブロックの上位3ビットが001になる必要があるため、nVersionの値は[0x20000000…0x3FFFFFFF]の範囲内となる。

BIP-34、BIP-66、BIP-65の制約により、nVersionの値として利用できるのは0x7FFFFFFBのみ。このため独立したデプロイは30個までという制約が付く。上位3bitを001に制限することで、この提案により30個中29個を取得し、将来の2つの異なるメカニズムのためのアップデートをサポートする(上位ビットが010と011)。ブロックのnVersionが上位ビット001でない場合、デプロイのためのビットは全て0として扱われる。

マイナーは変更の取込みが確認できるよう、LOCKED_INフェーズのビットをセットし続ける必要がある(このこと自体はコンセンサスルールには影響しない)。

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

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

状態遷移

遷移図

ジェネシスブロックは各デプロイのために状態 DEFINED を持つ。

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

retarget期間内の全てのブロックは全て同じ状態を持つ。これは、floor(block1.height / 2016) = floor(block2.height / 2016) の場合、各デプロイのため同じ状態を持つことが保証されている。

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

そうでない場合は次の状態は前の状態に依存する。

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

スタートタイムが過ぎるかタイムアウトになるまでは最初の状態(DEFINED)のままである。コード内のGetMedianTimePastの部分は、直近10ブロックのnTimeの中央値を取得する。 GetMedianTimePast(block.parent)式は↑の図のMTPの部分でチェーンによって定義される単調なクロックとして扱われる。

case DEFINED:
    if (GetMedianTimePast(block.parent) >= timeout) {
        return FAILED;
    }
    if (GetMedianTimePast(block.parent) >= starttime) {
        return STARTED;
    }
    return DEFINED;

STARTED状態になり期間がタイムアウトを過ぎていたらFAILEDに切り替わる。タイムアウト期間になっていない場合は、設定されているビットを集計し、過去の充分な数のブロックが、そのバージョン番号にデプロイのビットが設定されている場合、状態をLOCKED_INに遷移させる。この際の閾値は、mainnetの場合は(2016の95%である)1915ブロックで、testnetの場合は(2016の75%である)1512ブロック。同じビットを使った重複しないデプロイがある可能性があり、最初の1つがLOCKED_INに遷移し、もう1つがSTARTEDに移行している場合、両方のビットのセットを要求することになるだろう。

ブロックの状態はそのブロックのnVersionに依存しせず、常に先祖のブロックのものに依存することに注意すること。

case STARTED: {
    if (GetMedianTimePast(block.parent) >= timeout) {
        return FAILED;
    }
    int count = 0;
    walk = block;
    for (i = 0; i < 2016; i++) {
        walk = walk.parent;
        if (walk.nVersion & 0xE0000000 == 0x2000000 && (walk.nVersion >> bit) & 1 == 1) {
            count++;
        }
    }
    if (count >= threshold) {
        return LOCKED_IN;
    }
}

LOCKED_INのretarget期間後は自動的にACTIVEに遷移する。

case LOCKED_IN:
    return ACTIVE;

ACTIVEとFAILEDは最終の状態である。

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

実装においては、状態はブロックチェーンが分岐しても維持され、チェーンの再編成が行われる際は再計算が必要になることを注意しておく必要がある。

特定のブロックやデプロイの組み合わせの状態は、現状のretarget期間の前にその祖先によって決められることを考慮すると、各multiple-of-2016ブロックの状態の結果をキャッシュすることで、親によってインデックス化され、上記の仕組みを効率的&セーフティに実装することが可能。

ワーニングの仕組み

アップグレードへのワーニングをサポートするため、"implicit bit" mask = (block.nVersion & ~expectedVersion) != 0を使って、"unknown upgrade"が追跡される。Maskは予期せぬビットがnVersionにセットされた際に非ゼロになる。LOCKED_INで未知のアップグレードが検出されると、ソフトウェアは今後のソフトフォークについて大きく警告する必要がある。次のretarget期間になると更に大きく警告する必要がある。

まとめ&所感

  • Bitcoinのような分散型の仕組みだと、コンセンサスルールの変更を浸透させるのはシンドイ。
  • そんなBitcoinのコンセンサスルールの変更を容易にするために提案されたのがこのBIP-9。
  • BIP-9では複数のソフトフォークを同時にデプロイできるようにすることで、ソフトフォークのデプロイを容易にする。同時に最大29個のソフトフォークの展開が可能。
  • ブロック内のnVersionフィールドの解釈の仕方を変更し、version bitsという仕組みを導入することで実現。
  • ソフトフォークはその提案について、開始時間のタイムアウトが定められる。
  • version bitsではデプロイしたソフトフォークに状態が関連付けられ、その状態はDEFINEDSTARTEDLOCKEC_INACTIVEFAILEDの5つ。
  • 状態の各フェーズでタイムアウトするか決められた閾値に達しない場合はFAILEDになり、アクティベートできない。
  • Bitcoin Core 0.12.1では初めて、BIP-68、BIP-112、BIP-113のソフトフォークがversion bitsを利用してデプロイされた。
  • BItcoin Core 0.12.1でversion bitsを利用したソフトフォークのリリースでは、2016/5/1の深夜〜2017/5/1の間ブロックのバージョン番号が0x20000001に設定されるみたい。(ということは2016/5/1の深夜にstarttimeが設定されているということ?)
  • 現時点でBitcoin Core 0.12.1でgetblockchaininfoすると↓のようにBIP-9のソフトフォークの状態が確認できる。 softforksの部分が今までのブロックのバージョンをあげるISMによるソフトフォークで、bip9_softforksの部分がBIP-9のversion bitsを利用したソフトフォークの状態。
{
  "chain": "main",
  "blocks": 407906,
...
  "softforks": [
    {
      "id": "bip34",
      "version": 2,
      "enforce": {
        "status": true,
        "found": 1000,
        "required": 750,
        "window": 1000
      },
      "reject": {
        "status": true,
        "found": 1000,
        "required": 950,
        "window": 1000
      }
    }, 
    {
      "id": "bip66",
      "version": 3,
      "enforce": {
        "status": true,
        "found": 1000,
        "required": 750,
        "window": 1000
      },
      "reject": {
        "status": true,
        "found": 1000,
        "required": 950,
        "window": 1000
      }
    }, 
    {
      "id": "bip65",
      "version": 4,
      "enforce": {
        "status": true,
        "found": 1000,
        "required": 750,
        "window": 1000
      },
      "reject": {
        "status": true,
        "found": 1000,
        "required": 950,
        "window": 1000
      }
    }
  ],
  "bip9_softforks": [
    {
      "id": "csv",
      "status": "defined"
    }
  ]
}