Develop with pleasure!

福岡でCloudとかBlockchainとか。

ピアにトランザクションをリレーしないことを通知するdisabletxメッセージを定義したBIP-338

Bitcoin Coreはネットワークに接続すると8つアウトバウンドピアを選択するが、エクリプス攻撃などに対する堅牢性を高めるため、Bitcoin Core 0.19.0.1以降これにブロックリレーのみを行う2つのアウトバウンド接続(ブロックオンリーリレー)が追加されている。そのためアウトバウンド接続の合計数は10。

8つアウトバウンド接続ではこれまで通りトランザクションなど様々なP2Pメッセージを送信するが、このブロックオンリーリレーの2つのアウトバウンドピアにはブロックのリレーしかしない。そのため、これらの2つのピアに対して消費されるネットワーク帯域は他の接続に比べてとても低リソースになる。

このブロックオンリーリレーは、Bloom Filterを利用した軽量クライアントのプロトコルを定義したBIP-37↓

techmedia-think.hatenablog.com

で拡張された、ノード接続の開始時に交換するversionメッセージのfRelayフィールドをfalseにセットすることで実現している。ただ、これはもともと、軽量クライアントがフィルターをロードするまでの間に大量にinvトランザクションが通知されても困るので、それを停止させる目的で導入されたフィールドになる。このinvの停止はピアとの接続においてずっと保証されるものではなく、フィルター操作をするメッセージfilterloadfilteraddfilterclearが呼ばれると、停止していたinvが再開される。

この接続を受け入れたピアにとっても、途中からトランザクションが通知される可能性もあり、それがブロックオンリーリレーかどうか判断することができなくなってしまうので、今回新しくdisabletxというメセージを導入して、そのノードがトランザクションの通知をしないことを接続の初期化時に明示的に宣言できるようにしようというのがBIP-388の内容↓

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

基本的にdisabletxメッセージを送信した接続は、ブロックリレーのためのもので、Txに加えてaddr/getaddrメッセージなどを利用したアドレスのリレーをすることもなくなる。ただし、これらが送られてきたからといって、接続は切断することはないようになっている。このあたりは、このBIPを実装しないソフトウェアとの動作互換を保つためかな?またCompact Block(BIP-152)で必要なトランザクションを送受信することは可能になっている。

以下、BIPの意訳。

概要

このBIPでは、接続がトランザクションリレーに使用されないことをノードがピアに通知し、ネットワークで現在使用されているブロックオンリーリレーの接続をサポートするためのP2Pプロトコルの変更について説明する。

同期

この提案は、トランザクションをリレーするピアとリレーしないピアを区別することで、ピアがサービス可能なインバウンド接続数を増やすための取り組みの一環である。

2019年以降、Bitcoinネットワーク上の接続開始時にトランザクションリレーフィールド(BIP 37によって導入されBIP 60でも定義されている)をfalseに設定し、その接続においてトランザクションリレーが発生しないようにするソフトウェがデプロイされている*1。さらに、ピアからaddrメッセージを受信しても、このソフトウェアによって無視される。

この接続の目的は2つある。ブロックが伝播する低帯域幅の接続を追加することで、ネットワーク分断攻撃に対するノードの堅牢性が強化される。さらに、トランザクションをリレーせず、受信したアドレスを無視することで敵対者が完全なネットワークグラフ(もしくはサブグラフ)を学習する能力が低下するため*2、ネットワーク分断攻撃を実行しようとする敵対者にとっては(そのような知識を持っている場合に比べて)コストや難易度が高くなる。

この接続の低帯域幅/最小リソースの性質は、現在、接続の開始者のみが知っている。これはversionメッセージのトランザクションリレーフィールドが接続の存続期間中の永続的な設定ではないためだ。その結果、トランザクションリレーが無効になっているインバウンド接続を受信するノードは、トランザクションリレーを有効にしないピア(BIP 37で定義)と有効にするピアを区別できない。さらに、インバウンド接続がリレーされたアドレスを無視することも判断できない。その知識があれば、ノードは通知されたアドレスを代わりに受け取るために他のピアを選択する可能性がある。

この提案では、ノードがピアへの接続を開始する際に、接続が存続中はトランザクションリレーに使用されるべきではないことを示す新しいオプションメッセージを追加する。さらに接続において、アドレスをリレーすべきかどうかをネゴシエートする仕組みが現在ないため、このBIPではトランザクションリレーが無効化されたリンクにおいてはアドレスメッセージを送信しないよう提案する。

このBIPがデプロイされると、ノードは(disabletxを送信するような)低リソースノードとフルリレーピアを区別するインバウンド接続制限をより簡単に実装でき、ネットワーク上で可能なブロックオンリーリレーの接続数を増やすことができる。

仕様

  1. 新しくdisabletxメッセージが追加される。このメッセージはメッセージタイプがdisabletxの空のメッセージとして定義される。
  2. このBIPを実装するノードのプロトコルバージョンは70017以上に設定する必要がある。
  3. ノードがversionメッセージのトランザクションリレーフィールドをfalseにセットした場合、ピアのプロトコルバージョンが70017以上であれば、そのピアからversionメッセージに応答してdisabletxメッセージも送信される可能性がある。送信された場合、verackを送信する前にdisabletxメッセージを送信しなければならない。
  4. ピアとの間でdisabletxメッセージを送受信したノードは、以下のメッセージをピアに送信してはならない:
    1. トランザクションinvメッセージ
    2. トランザクションに対するgetdataメッセージ
    3. merkleblockに対するgetdataメッセージ(BIP 37)
    4. filteradd/filterload/filterclear(BIP 37)
    5. mempool(BIP 35)
    6. tx メッセージ
  5. ピアとの間でdisabletxメッセージを送受信したノードは、以下のメッセージをピアに送信しないことを推奨する:
    1. addr/getaddr
    2. addrv2(BIP 155)
  6. 他のメッセージタイプの送信または処理に関する動作は、このBIPでは指定されていない。
  7. ノードはこのメッセージを送るピアとの接続を維持しない可能がある(例えば、トランザクションをリレーするピアを見つける場合など)。

互換性

このBIPを実装していないプロトコルバージョン>= 70017のノードおよび、プロトコルバージョン< 70017のノードは、実装ソフトウェアとの互換性を維持する。トランザクションdisabletxメッセージを送信するピアにリレーされず(BIP 37もしくはBIP 60が実装されている場合)、定期的なアドレスのリレーは引き続き行われる可能性はあるが、このBIPを実装するソフトウェアは、その理由だけで、そのようなピアとの接続を切断すべきではない。

アドレスリレーのネゴシエーション方法をより慎重に指定する可能性のある将来のプロトコル拡張を可能にするたえ、アドレスリレーの無効化を提案するがこのBIPでは必須ではない。アドレスをリレーしないソフトウェアに関するこのBIPの推奨事項は、既存のソフトウェアの動作に対応するため、そのような将来のプロトコル拡張がない場合のガイダンスとして解釈されることを意図している。

blocktxnおよびgetblocktxnを含むBIP 152で定義されたすべてのメッセージは、BIP 152の機能ネゴシエーションに従って、disabletxメッセージを送受信したピア間で許可されていることに注意すること。

この提案はBIP 37と互換性があるが、独立している。

実装

https://github.com/bitcoin/bitcoin/pull/20726

*1:Bitcoin Coreは2019年11月にリリースされたバージョン0.19.0.1以降、この機能を実装している

*2:例えば、https://www.cs.umd.edu/projects/coinscope/coinscope.pdfhttps://arxiv.org/pdf/1812.00942.pdf を参照。

Bech32の問題を修正した改良版Bech32m(BIP-350)

Segwit導入にあたって新しいアドレス(bc1から始まるアドレス)のエンコーディング方式として導入されたのがBIP-173として定義されたBech32

techmedia-think.hatenablog.com

Bech32にはタイプミスなどのアドレスの間違いが検出できるチェックサムが含まれているが、チェックサムを使った検証を回避する予期しない問題が含まれていた。

この問題は、pで終了する有効なBech32データについて、その前にqを挿入したり、qpで終了する場合はqを削除してもチェックサムは有効と判断されてしまういうもの。

現在導入されているsegwit version 0のアドレスについては、データ長が固定であるため、データ長のチェックをすれば基本的に問題ないが、Lightning Networkのインボイスなど可変長なデータのエンコードにBech32を利用している場合は影響する(まぁBech32の本来の定義ではデータ長は最大90文字という制限があるにも関わらずインボイスにBech32を採用してるので、採用根拠に疑問もあるけど)。

Taprootの導入にあたって、↑の問題に対処するためBech32の改良版であるBech32mがBIP-350として定義された↓

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

Segwitアドレスの内、segwit version 1以降のアドレスについてはこのBech32mが使用されることになる。なお、segwitのwitness version 0=P2WPKHとP2WSHのアドレスは既存のBech32のまま。

Bech32mでは、Bech32のチェックサムの修正が行われているが、大きな仕様変更ではなく、チェックサム計算時にXORする定数を1から0x2bc830a3に置き換えているだけ。

以下、BIP-350の仕様の訳。

仕様

最初に、新しいチェックサムアルゴリズムを定義し、続いてそれを将来のBitcoinアドレスでどのように使用するか文書化する。

Bech32m

Bech32mは、Bech32のチェックサムの仕様を変更し、最後にチェックサムにxorされている定数10x2bc830a3に置き換える。結果のチェックサムの検証およびチェックサムの作成アルゴリズムは:

BECH32M_CONST = 0x2bc830a3

def bech32m_polymod(values):
  GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
  chk = 1
  for v in values:
    b = (chk >> 25)
    chk = (chk & 0x1ffffff) << 5 ^ v
    for i in range(5):
      chk ^= GEN[i] if ((b >> i) & 1) else 0
  return chk

def bech32m_hrp_expand(s):
  return [ord(x) >> 5 for x in s] + [0] + [ord(x) & 31 for x in s]

def bech32m_verify_checksum(hrp, data):
  return bech32m_polymod(bech32m_hrp_expand(hrp) + data) == BECH32M_CONST

def bech32m_create_checksum(hrp, data):
  values = bech32m_hrp_expand(hrp) + data
  polymod = bech32m_polymod(values + [0,0,0,0,0,0]) ^ BECH32M_CONST
  return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]

他の部分は、 human-readable part (HRP)を含めてBech32から変更されていない。

Bech32とBech32m両方を同時にデコードする関数は、以下のように書ける:

class Encoding(Enum):
    BECH32 = 1
    BECH32M = 2

def bech32_bech32m_verify_checksum(hrp, data):
    check = bech32_polymod(bech32_hrp_expand(hrp) + data)
    if check == 1:
        return Encoding.BECH32
    elif check == BECH32M_CONST:
        return Encoding.BECH32M
    else:
        return None

デコードに失敗した場合はNoneを返し、成功した場合はECH32 / BECH32Mの列挙値を返す。

Segregated witnessアウトプットのアドレス

バージョン0のアウトプット(具体的にはP2WPKHとP2WSHアドレス)は、BIP173で定義されているようにBech32*1を引き続き使用する。Segregated witnessアウトプットバージョン1〜16までのアドレスは、Bech32mを使用する。ここでも、HRP bc を含むエンコーディングに関する他の機能は同じまま。

Segregated witnessアウトプットのアドレスを生成するには、

アドレスをデコードするには、クライアントソフトウェアは、Bech32とBech32m両方のデコーダーを使ってデコードするか*2、両方をサポートするデコーダーのいずれかを使用する。どちらの場合も、デコーダーはエンコーディングがデコードされたwitness version(Bech32の場合はバージョン0、その他のバージョンはBech32m)が期待する値と一致するか検証しなければならない。

以下のコードは、実行する必要があるチェックを示している。呼び出されたコードの詳細については、以下の参照実装セクションでリンクされているPythonのコードを参照。

def decode(hrp, addr):
    hrpgot, data, spec = bech32_decode(addr)
    if hrpgot != hrp:
        return (None, None)
    decoded = convertbits(data[1:], 5, 8, False)
    # Witness programs are between 2 and 40 bytes in length.
    if decoded is None or len(decoded) < 2 or len(decoded) > 40:
        return (None, None)
    # Witness versions are in range 0..16.
    if data[0] > 16:
        return (None, None)
    # Witness v0 programs must be exactly length 20 or 32.
    if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
        return (None, None)
    # Witness v0 uses Bech32; v1 through v16 use Bech32m.
    if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M:
        return (None, None)
    # Success.
    return (data[0], decoded)
エラー箇所

Bech32mはBech32と同様、いくつかの置換エラーの位置を特定できる*3。この機能を、このドキュメントで提案されているsegregated witnessアドレスと組み合わせるには、単純にBech32とBech32m両方でエラー位置を検索する。片方だけがエラー位置を見つけた場合、その片方を報告し、両方ともエラーを見つけた場合(非常に稀なことだが)、いくつかのオプションがある:

  • 結果が異なる場合、修正が少ないものを報告する。
  • 矛盾しているレスポンスを削除する。エラー位置にないシンボルは全てチェックできる。例えば、witness versionのシンボルがエラー位置ではなく、仕様(Bech32では0、Bech32mでは1+)に対応していない場合、その応答を削除できる。

これらの例については、以下のJavascriptデコーダーを参照。

互換性

このドキュメントでは、v1 segregated witnessアウトプット以上のバージョンの新しいエンコーディングを紹介する。受信側には互換性の問題はないはず。これらのアウトプットタイプはまだmainnetでは使用できないため、ウォレットはまだv1 segregated witnessアドレスをまだ作成していない。

一方、Bech32mの提案はv1以降のsegregated witnessアドレスの前方互換性を破っている。この非互換は意図的なものだ。代替案としてBech32を将来のアドレスの特定のサブセットで使用することも検討されたが、最終的に破棄された。このクリーンブレークにより、新しいアドレスは既存のBech32アドレスの検証と互換性がなくなるため、新しいソフトウェアだけでなく既存の送信者も突然の変更の問題から保護することができる。Taprootの支持者による実験では、バージョン0より高いsegregated witnessアウトプットのバージョンへの送金をサポートするウォレットやサービスはほとんどないため、前方互換性を破ることで失われるものはほとんどない。さらにこれらの実験では、segregated witnessの実装により、バージョン1のアドレスに送信する際にウォレットが資金をバーンしてしまうケースが特定された。このケースがそのまま使われる場合には、この選択されたアプローチにより、そのようなソフトウェアがBech32mアドレスに送信しようとした際に、資金の破壊を防ぐだろう。

参照実装

Test Vector

実装のアドバイス BIP173の実装をテストした実験では、多くのウォレットやサービスが高いバージョンのsegregated witnessアウトプットへの送信をサポートしていないことが分かった。ネットワーク上にv1 segregated witnessアウトプットを導入するTaprootのソフトフォークの提案を見越して、実装がv1以降のバージョンへの送信をサポートしていることを確認するとともに、以下に示すTest Vectorの完全なセットを採用することを強く推奨する。ネイティブのsegregated witnessアウトプットのすべての上位バージョンは、ネットワーク上では定義されていないため、ウォレットがそれらを作成したり、受信者が送信者に提供したりすることはできない。また、受信者は自分自身を対象とした支払いがバーンされるだけなので、誤ってそれらを提供したいと思うこともない。ただし、より上位バージョンを有効な受信者として定義することで、ネイティブsegwitアウトプットの上位バージョンを導入する将来のソフトフォークは、Bech32mの仕様を正しく実装するすべてのウォレットと前方互換性がある。

※ 実際のTest Vectorの値はBIP本体を参照。

Appendix:チェックサムの設計と特性

チェックサムはデータを送る途中で入り込んだエラーを検出するのに使われる。Base58Checkのようなハッシュ関数ベースのチェックサムはあらゆるタイプのエラーを均一に検出するが、実際にはすべての種類のエラーが同じように発生することはない。Bech32は置換エラーの検出を優先するが、ある種類のエラーの検出を優先すると、必然的に他の種類のエラーの検出が悪化する。Bech32の設計では、置換以外の単純なエラーパターンはハッシュ関数ベースの設計と同様の検出率を持ち、複雑で実用的でないエラーの検出率が悪くなるだけであるよう仮定していた。Bech32で発見された挿入の弱点は、これが当てはまらなかったことを示している。

Bech32mの場合、置換エラーに対するBech32の保証を維持しつつ、他の一般的なエラーがハッシュ関数ベースのチェックサムよりも悪くならないようにすることを目的としている。新しい標準が実装しやすいように、意図した置換エラーの検出を維持しつつ、q挿入問題を緩和するのに十分であることが観察されたため、設計の変更をxorされる最終的な定数の修正のみに制限している。続いて、新しい定数0x2bc830a3がどうやって選ばれたか説明する。

エラーパターンと検出確率

我々はエラーパターンを、最初に1つ以上の削除、次に隣接する文字の入れ替え、続いて、置換、挿入、重複の順で、特定の位置にあるものとして定義し、有効なチェックサムを持つ文字列に適用されるか、そうでなければランダムに選択される。挿入と置換については、一様にランダムな新しい文字を想定する。例えば、「17番目の文字を削除し、11番目の文字と12番目の文字を交換し、24番めにランダムな文字を挿入する」のはエラーパターンになる。「43番めから48番めの文字をaardvarkに置き換える」のは有効なエラーパターンではない。新しい文字はランダムではなく、この特定の文字列が他のどの文字列よりも秋変えられる可能性が高い理由がないためだ。

30ビットのハッシュを使ったハッシュ関数ベースのチェックサムの設計では、すべてのエラーパターンに対して、 {2^{-30}}で値を誤って受け入れる可能性がある。Bech32は、最大4つの置換で構成されるエラーパターンを誤って受け入れる確率は0であり、常に検出される。q挿入の問題は、Bech32の場合、確率 {2^{-10}}の単純なエラーパターン(最後から2番めの位置にランダムな文字を挿入)が存在することを示している。この時、最後の文字はpで(32文字の中の1つだけ)、挿入される文字はq(32の挿入可能な文字で1つだけ)でなければならない。

エラーパターンが何であるか(どのような種類のエラーがどこで発生するか)の選択は、確率の一部ではないことに注意してほしい。我々はランダムに選択されたものだけでなく、すべてのパターンが上手く動作することを確認しようとしているが、おそらく人間はある主のエラーを他のエラーよりも多く起こし、それを簡単にモデル化することができないからだ。

Bech32mの検出特性

以下の表で、Bech32mのエラー検出特性とBech32の比較を示す。この分析に使用されたコードはここにある。各行は、左4列の制約を介した1つのエラーパターンを指定する。残りの列はこれらのパターンの何%が検出されない特定の確率を持っているか示す。列は以下の通り:

  • errors: 考慮される個々のエラーの最大数。
  • of type: 考慮されるエラーのタイプ(置換のみの場合の「subst. only」か、削除、交換、挿入、重複を含む「any」のいずれか)。
  • window: エラーが発生しなければならないウィンドウの最大サイズ*4
  • code/verifier: この行ががBech32もしくはBech32mでエンコードされた文字列かどうか、およびそれらがBech32もしくはBech32mのいずれかの検証によって受け入れられる確率について評価されるかどうか*5*6
  • error patterns with failure probability: 各確率( {0, 2^{-30}, 2^{-25}, 2^{-20}, 2^{-15}, 2^{-10}})について、前の列の制約によって制限されたエラーパターンの何%が誤って受け入れられる確率を持っているか。

特性は2つの種類に分類される。すべてのとりうるHRPを平均化したときに全ての文字にわたって保持されるものと、segregated witnessアドレス*7によって課される長さ制限を持つHRPbc1固有のものだ。

※ 表自体はBIP本体を参照。

表内の数値および以前の定数1との比較は、こちら

選定プロセス

選定プロセスの詳細はここにあるが、簡単に言うと:

  • Bech32の1とは異なる {2^{30}-1}の集合から検討する。これらの定数はすべて上の表の(a)でマークされた特性を満たしている。
  • 徹底的な分析により、上の表の(b)でマークされた特性を示さない *8。 すべての定数(例えば検出確率 {2^{-20}}で68文字以下のウィンドウ内で2つ以下のエラーのエラーパターンを許容するすべての定数)を除外する。この選択により12054の候補に絞られる。
  • 上の表の(c)の特性を示さない全ての定数を除外することで *9 、候補は79個になる。
  • 最後にタイブレーカーとして、上の表の(d)に一致するエラークラスの数を最小にする候補を選択する。結果が単一の定数0x2bc830a3

脚注

*1:v0アドレスにBech32とBech32mの両方を許可しない理由は?両方を許可するとエラーの検出機能が低下する(29ビットのチェックサムしかないことと同等)

*2:1つの文字列をBech32とBech32mとして両方同時に有効にすることは可能か?いいえ、有効なBech32とBech32mの文字列は同じ長さの場合、常に少なくとも3文字は異なる。

*3:エラー訂正についてはどうか?BIP-173で説明されているようにエラー訂正を導入すると、エラーを検出する機能が低下する。Bech32(m)のBCH符号としての性質により、技術的には少数のエラーを修正することは可能だが、実装では、エラーが存在する可能性のある場所を示す以上の目的でこれを使用するのは控えること。

*4:エラーパターンのウィンドウサイズとは?エラーパターンのウィンドウサイズは、変更された全ての文字を含む最小の連続範囲の長さになる(入力もしくは出力でもいずれか大きい方)。例えば「abcdef」を「accdbef」に変換するエラーパターンは「bcd」を4文字の[ccdb]に置き換えているため、ウィンドウサイズは4になる。ウィンドウサイズはパターンが2つ以上のエラーで構成されている場合にのみ意味がある。

*5:Bech32の検証でBech32m文字列を受け入れる可能性を気にするのはなぜ?Bech32mが既存のBech32の使用を置き換えるアプリケーションの場合、たとえ少数のエラーが発生したとしても新しいソフトウェアによって作成されたBech32m文字列が、Bech32を想定する古いソフトウェアによって受け入れられないようにする必要がある。

*6:有効なBech32m文字列を取得することで障害が発生し、発生後Bech32の検証は受け入れられるケースも考慮する必要がある?この状況は、理論的にはv1アドレスのバージョン番号をv0に変更するエラーが発生した際のsegregated witnessアドレスで発生する可能性がある。このタイプのエラーの特異性に加えて、v0アドレスに適用される追加の制約のため、これは可能性は低く、分析が困難だ。

*7:bc1固有の分析ではどのような制限が考慮された?(witness programが少なくとも2バイトなので)最小長と、(witness programは最大40バイトであるため)最大長と、witness programは8ビットの倍数であるという事実、最初のデータシンボルは16を超えることはできないという事実や、パディングが0でなければならないという事実はすべて考慮されない

*8:特性はどうやって選択された?これらの特性はすべて、確率が低い定数の拒否やエラーの多い定数、もしくはウィンドウが広い定数を拒否するなどのすべての定数を拒否することなく、可能な限り協力だ

*9:HRP bc1を使用したsegregated witnessアドレスに特化して最適化するのはなぜ?一般的なHRPの解析には制限がある。ここの"Technical details"を参照。我々はまず一般的な使用法に対して最適化をするが、segregated witnessアドレスに対してはタイブレーカーとして最適化を行う

Guixを使った決定性ビルド

先日、Bitcoin CoreをGitian Buildを使ってビルドする記事を書いた↓

techmedia-think.hatenablog.com

けど、最近以下のPRがBitcoin Coreにマージされた結果、Linux / Windows / macOSのバイナリのビルドがGNU Guixでできるようになった。

github.com

GitianとGuix

Gitianは元々Bitcoin Coreで再現性のあるビルドをできるよう開発されたツール。ただこのツール、VMを使ってバイナリをビルドするんだけど、Ubuntuに結構依存している。Bitcoinのような管理者のいないコードベースのエコシステムにおいては、監査可能性や透明なバイナリを提供するのが大切で、環境依存はなるべく少ない方が望ましい。

そのため既存のGitianベースのビルドの仕組みを完全に置き換えるのに、GNU Guixベースのビルドを導入しようという流れみたい。Guixはクロスプラットホームのパッケージ管理機能を提供するオープンソースのツールで、再現性のあるビルドをする機能もその機能の一部。

Guixを使ったBitcoin Coreのビルド

今回はGuixを使ってBitcoin CoreのLinux / Windows / macOSバイナリを生成してみよう(環境はUbuntu 20.04 LTS)。ガイドはこちら

Guixのインストール

インストールガイドを参考にインストールする。今回はダウンロード、インストール、初期設定を自動で行うスクリプトを使ってインストールする。

準備として、Guix開発者のPGP公開鍵をインポートする。

$ wget 'https://sv.gnu.org/people/viewgpg.php?user_id=15145' -qO - | sudo -i gpg --import -

続いて、スクリプトを使ってインストール。

$ cd /tmp
$ wget https://git.savannah.gnu.org/cgit/guix.git/plain/etc/guix-install.sh
..
$ chmod +x guix-install.sh
$ sudo ./guix-install.sh
..

インストールが終わったら、日本語環境なのでglibc-localesをインストールしとく。

$ guix install glibc-locales

最後に、.zshrcbashの場合は.bashrc)に以下を追加。

source $HOME/.guix-profile/etc/profile
export GUIX_LOCPATH="$HOME/.guix-profile/lib/locale"

単純にテストしたい場合は、便利なdocker環境が用意されているので、それ使うのもあり。

macOS用のSDKを配置

Gitianと同様XcodeからSDKを抽出する必要がある。抽出方法は、↑のGuitianの記事参照。SDKbitcoin/depends/SDKs以下に展開する。

$ mkdir <cloneしたbitcoinのパス>/depends/SDKs
$ cp Xcode-11.3.1-11C505-extracted-SDK-with-libcxx-headers.tar.gz <cloneしたbitcoinのパス>/depends/SDKs
$ cd <cloneしたbitcoinのパス>/depends/SDKs
$ tar -xvf Xcode-11.3.1-11C505-extracted-SDK-with-libcxx-headers.tar.gz

ビルド

Bitcoin CoreにはGuixを使ってビルドするためのスクリプトが用意されているので、基本的にそれを叩くだけ↓

$ ./contrib/guix/guix-build.sh

このスクリプトが実行するのは、

  1. bitcoin/dependsの必要なソースコードのダウンロード
  2. guix time-machineコマンドを使ってコンテナ上でバイナリをビルド(初回はかなり時間がかかる)

デフォルトで以下の環境向けのバイナリが生成される:

↑を全部ビルドしたくない場合は、環境変数HOSTSにビルドしたいものをスペース区切りでセットすればいい。

現状は現状のローカルリポジトリの最新コミットに対してビルドが実行されるっぽい。ビルドを実行すると、x86_64-linux-gnuであればdistsrc-6a726cb534ed-x86_64-linux-gnuといったディレクトリが作られ、ソースがコピーされる。6a726cb534edはコミットのハッシュ値の一部で、同じコミットのビルドを実行しようとすると、これらのディレクトリがあるとエラーで実行されない。特定のタグのビルドが作りたければ、そのタグをチェックアウトして実行するんだろう。

実際に実行されるguix time-machineコマンドは↓

time-machine environment --manifest="${PWD}/contrib/guix/manifest.scm" \
     --container \
     --pure \
     --no-cwd \
     --share="$PWD"=/bitcoin \
     --share="$DISTSRC_BASE"=/distsrc-base \
     --share="$OUTDIR"=/outdir \
     --expose="$(git rev-parse --git-common-dir)" \
     ${SOURCES_PATH:+--share="$SOURCES_PATH"} \
     --max-jobs="$MAX_JOBS" \
     ${SUBSTITUTE_URLS:+--substitute-urls="$SUBSTITUTE_URLS"} \
     ${ADDITIONAL_GUIX_COMMON_FLAGS} ${ADDITIONAL_GUIX_ENVIRONMENT_FLAGS} \
     -- env HOST="$host" \
            MAX_JOBS="$MAX_JOBS" \
            SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:?unable to determine value}" \
            ${V:+V=1} \
            ${SOURCES_PATH:+SOURCES_PATH="$SOURCES_PATH"} \
            DISTSRC="$(DISTSRC_BASE=/distsrc-base && distsrc_for_host "$HOST")" \
            OUTDIR=/outdir \
          bash -c "cd /bitcoin && bash contrib/guix/libexec/build.sh"

ビルドは分離されたコンテナで実行される。使われるコンテナには、各環境毎のディレクトリ内のマニュフェストファイル(例:distsrc-6a726cb534ed-x86_64-linux-gnu/contrib/guix/manifest.scm)で定義されたパッケージがセットアップされる。

Guixでコンテナがセットアプされると、そのコンテナ上で↑の最後に記載されているコマンドbash -c "cd /bitcoin && bash contrib/guix/libexec/build.sh"が実行される。

ビルドが終わる、ホストOS側のoutputディレクトリ以下に各環境毎のバイナリが生成されている。

$ ls output
bitcoin-6a726cb534ed-aarch64-linux-gnu-debug.tar.gz    bitcoin-6a726cb534ed-riscv64-linux-gnu.tar.gz
bitcoin-6a726cb534ed-aarch64-linux-gnu.tar.gz          bitcoin-6a726cb534ed-win-unsigned.tar.gz
bitcoin-6a726cb534ed-arm-linux-gnueabihf-debug.tar.gz  bitcoin-6a726cb534ed-win64-debug.zip
bitcoin-6a726cb534ed-arm-linux-gnueabihf.tar.gz        bitcoin-6a726cb534ed-win64-setup-unsigned.exe
bitcoin-6a726cb534ed-osx-unsigned.dmg                  bitcoin-6a726cb534ed-win64.zip
bitcoin-6a726cb534ed-osx-unsigned.tar.gz               bitcoin-6a726cb534ed-x86_64-linux-gnu-debug.tar.gz
bitcoin-6a726cb534ed-osx64.tar.gz                      bitcoin-6a726cb534ed-x86_64-linux-gnu.tar.gz
bitcoin-6a726cb534ed-riscv64-linux-gnu-debug.tar.gz    src

ブロックチェーンに埋め込まれているBitcoinのホワイトペーパーをデコードしてみる

CWSによる著作権の訴えで最近話題になってるBitcoinのホワイトペーパーについて、Bitcoin Optechのニュースレターで取り上げられてたStackExchangeの記事↓

bitcoin.stackexchange.com

が面白かったので、Bitcoinブロックチェーンに埋め込まれているBitcoinのホワイトペーパーをデコードしてみる。

Bitcoinのホワイトペーパーのデータが記載されているトランザクションは、2013-04-07のブロック230009に格納された54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713。サイズが400KB弱とBitcoinトランザクションとしては巨大なトランザクションだ。

このトランザクションは1つのインプットと948個のアウトプットを持っている。このアウトプットの内、945個が以下のような1 of 3のrawマルチシグのアウトプットで(↓は最初のアウトプット)、

1 e4cf0200067daf13255044462d312e340a25c3a4c3bcc3b6c39f0a322030206f626a0a3c3c2f4c656e6774682033203020522f46696c7465722f466c6174654465 636f64653e3e0a73747265616d0a789cad5c4b8b24b911becfafa8b3a1da292925654253d0d55373f06d61c007e39bbd061f0cde8bffbe25c55b5266f61ab3905d 9ba54728e28bb76a963777fbcfb77fdf96db7d291f93f3e599f7fafcedefb73fffe1f6aff665fdefb77f7c7bfefce6c2fa166e695bdfd6dbcfbfddfef8c3dd5cf9 3 OP_CHECKMULTISIG

そして946個めが1つだけ1 of 1のrawマルチシグ。残り2つはP2PKHアウトプット。

なお、PDFのデータは%PDF-で始まり、これをhex値にすると255044462d。↑の3 of 3のマルチシグの最初の公開鍵

e4cf0200067daf13255044462d312e340a25c3a4c3bcc3b6c39f0a322030206f626a0a3c3c2f4c656e6774682033203020522f46696c7465722f466c6174654465

にこのデータが含まれているのが分かる。この255044462dを起点として、すべてのマルチシグスクリプトの公開鍵を連結するとBitcoinのホワイトペーパーが生成できる!

bitcoinrbを使ってデコード処理を書くと以下のようになる(トランザクションのhexデータを同じディレクトリの54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713というファイルに保存してる前提)。

require 'bitcoin'

tx = Bitcoin::Tx.parse_from_payload(File.read(File.join(File.dirname(__FILE__), '54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713')).htb)

File.open('bitcoin.pdf', mode = 'w') do |f|
  tx.outputs[0...-2].map.with_index do |o, i|
    data = o.script_pubkey.get_multisig_pubkeys.map.with_index do |p, j|
      if i == 0 && j ==0
        p[(p.index("%PDF-"))..-1]
      elsif i == 945
        p[0...-8]
      else
        p
      end
    end.join
    f.write data
  end
end

実行すると同じディレクトbitcoin.pdfが生成される。

最後のマルチシグの公開鍵はサイズを合わせるため8バイト分0が埋まってるので、それを取り除いたデータサイズは184292バイトになる。ちなみに、最初の公開鍵の255044462dの前にあった先頭8バイトe4cf0200067daf13の内、先頭4バイトe4cf0200は、このサイズをリトルエンディアンで表したもので、後半4バイト067daf13は生成したPDFのCRC32の値をリトルエンディアンで表したものになる。

もともとBitcoinのホワイトペーパー自体はそんなに長いペーパーではなかったけど、1トランザクションに埋め込めるものなのね。

ちなみに、StackExchangeの回答にはフルノードのRPCを実行して、これを1行で行うコマンドが掲載されてる

Gitian Buildで決定性ビルドを実行する

少し前にBitcoin Core v0.21.0がリリースされたけど、このBitcoin CoreのリリースにはGitian Buildという決定性ビルドプロセスが使われている。

https://github.com/bitcoin/bitcoin/blob/master/doc/release-process.md

Gitian Buildとは?

公式サイトによると、複数のビルダーが、同一のバイナリを生成するビルドツールで、それぞれのビルダーが同じバイナリに署名し、バイナリが同じソースから生成されたもので改竄されていないことを保証する。

Gitian Builderとは?

Gitian Builderは、qemuベースのVMでソフトウェアをビルドする際に使用するツール。現在サポートされているVMは、

  • KVM
  • LXC
  • Docker
  • Virtual Box

の4つ。今回はDocker使ってみる。

Bitcoin Coreのビルド

先日リリースされたBitcoin Core v0.21.0をGitian Buildでビルドしてみよう。ビルド方法はこのドキュメントに記載されてる。

セットアップ

Bitcoin Coreには、gitian-build.pyという自動スクリプトが用意されているので、これを使う。

まず、スクリプトをコピー。

$ cp bitcoin/contrib/gitian-build.py .

続いて、セットアップを実行(-dはdockerを使用するオプションで、-kだとkvm、指定されてなかったらlxc)。

$ ./gitian-build.py -d --setup

すると、必要なソフトウェアがインストールされ、以下のリポジトリがcloneされる(最初にスクリプトをコピーしたのは、スクリプトディレクトリにこれらがcloneされるから)。

自分のPGP鍵で署名するには、gitian.sigsリポジトリをフォークして、リモートリポジトリとして登録する↓

$ export NAME = azuchi
$ cd gitian.sigs
$ git remote add $NAME git@github.com:$NAME/gitian.sigs.git
mac用のビルドへの署名用のセットアップ

mac用のビルドに署名する場合は、追加のセットアップが必要になる。まず、Appleのダウンロードページから*1Xcode_11.3.1.xipをダウンロードする*2Xcodeのバージョン7以降は、非macOSSDKを展開するのが楽になるようXcode.appから.xipに変わったらしい。Ubuntuでは以下の方法で、Xcode.appを展開し、Xcode-11.3.1-11C505-extracted-SDK-with-libcxx-headers.tar.gzを生成する(Xcode_11.3.1.xipは同じディレクトリに配置しとく)。

$ sudo apt-get install cpio
$ git clone https://github.com/bitcoin-core/apple-sdk-tools.git
$ python3 apple-sdk-tools/extract_xcode.py -f Xcode_11.3.1.xip | cpio -d -i

↑が終わると、Xcode.appというディレクトリが作成されており、Xcodeのデータが展開されてる。ちなみにmacOSの場合は、xip -x Xcode_11.3.1.xipを実行するだけ。

続いて、bitcoingen-sdkスクリプトを使って、Xcode-11.3.1-11C505-extracted-SDK-with-libcxx-headers.tar.gzを生成する。

$ ./bitcoin/contrib/macdeploy/gen-sdk 'Xcode.appのパス'

実行ディレクトリに↑のファイルが生成されるので、それをgitian-builder/inputsに配置する。

$ mkdir -p gitian-builder/inputs
$ cp Xcode-11.3.1-11C505-extracted-SDK-with-libcxx-headers.tar.gz gitian-builder/inputs

ビルド

Bitcoin Core 0.21.0をビルドするためには↓を実行する。

$ export VERSION=0.21.0
$ ./gitian-build.py -j 3 -m 5000 -d --detach-sign --no-commit -b $NAME $VERSION

各オプションは:

  • -jオプションはVMに割り当てるコア数+1。
  • -mVMに割り当てたRAMより少し小さい値(MB単位)で、-jと合わせてビルドの高速化のためのオプション。
  • --detach-signは、デタッチ署名のアサートファイルを作成し、何もコミットしないオプション。
  • --no-commitは、gitに何もコミットしないオプション。
  • -bは、Gitian Buildを行う指示。

ちなみにこの実行にはマシンスペックにもよるけどかなり時間かかる。実行が終わるとbitcoin-binaries/0.21.0LinuxmacOSWindows用の各バイナリが生成されている。

$ ls bitcoin-binaries/0.21.0 
bitcoin-0.21.0-aarch64-linux-gnu-debug.tar.gz    bitcoin-0.21.0-riscv64-linux-gnu.tar.gz
bitcoin-0.21.0-aarch64-linux-gnu.tar.gz          bitcoin-0.21.0-win64-debug.zip
bitcoin-0.21.0-arm-linux-gnueabihf-debug.tar.gz  bitcoin-0.21.0-win64-setup-unsigned.exe
bitcoin-0.21.0-arm-linux-gnueabihf.tar.gz        bitcoin-0.21.0-win64.zip
bitcoin-0.21.0-osx-unsigned.dmg                  bitcoin-0.21.0-x86_64-linux-gnu-debug.tar.gz
bitcoin-0.21.0-osx64.tar.gz                      bitcoin-0.21.0-x86_64-linux-gnu.tar.gz
bitcoin-0.21.0-riscv64-linux-gnu-debug.tar.gz    bitcoin-0.21.0.tar.gz

実行中のログにも表示されるが、これらのバイナリのハッシュ値は、公開されているBitcoin Coreのダウンロードサイトに記載されているハッシュ値と等しい。

そして、gitian.sigs/に以下の3つのassertファイルが生成されてる。

  • 0.21.0-linux/azuchi/bitcoin-core-linux-0.21-build.assert
  • 0.21.0-osx-unsigned/azuchi/bitcoin-core-osx-0.21-build.assert
  • 0.21.0-win-unsigned/azuchi/bitcoin-core-win-0.21-build.assert

以下のコマンドでこれらに署名する。

$ gpg --output ${VERSION}-linux/${NAME}/bitcoin-core-linux-${VERSION%\.*}-build.assert.sig --detach-sign ${VERSION}-linux/$NAME/bitcoin-core-linux-${VERSION%\.*}-build.assert
$ gpg --output ${VERSION}-osx-unsigned/$NAME/bitcoin-core-osx-${VERSION%\.*}-build.assert.sig --detach-sign ${VERSION}-osx-unsigned/$NAME/bitcoin-core-osx-${VERSION%\.*}-build.assert
$ gpg --output ${VERSION}-win-unsigned/$NAME/bitcoin-core-win-${VERSION%\.*}-build.assert.sig --detach-sign ${VERSION}-win-unsigned/$NAME/bitcoin-core-win-${VERSION%\.*}-build.assert

すろと、以下の3つの署名ファイルが生成される。

  • 0.21.0-linux/azuchi/bitcoin-core-linux-0.21-build.assert.sig
  • 0.21.0-osx-unsigned/azuchi/bitcoin-core-osx-0.21-build.assert.sig
  • 0.21.0-win-unsigned/azuchi/bitcoin-core-win-0.21-build.assert.sig

各OS毎の.assertファイルと.assert.sigファイルをコミットし、bitcoin-core/gitian.sigsリポジトリにPRを送ると。

$ git checkout -b ${VERSION}-not-codesigned
$ git commit -S -a -m "Add $NAME $VERSION non-code signed signatures"
$ git push --set-upstream $NAME $VERSION-not-codesigned

そしてコード署名者のみ、Windows/macOSのdetached signaturesを作成する。コード署名をするのは1人のみで、Windows/macOSのビルドがそれぞれ3つの一致する署名がある場合のみ、それぞれのリリース鍵で署名できる。

コード署名者以外は、Windows/macOSのdetached signaturesが作られるのを待つ。detached signaturesは、bitcoin-detached-sigsリポジトリにコミットされるらしいが、リポジトリ見る限り0.11.0以降はコミットされてないっぽい(プロセスが変わってる?)。

その後、署名されたWindows/macOS用のバイナリを作成する。さらに各ビルダーは署名されたバイナリへの署名を作成する。この署名がgitian.sigsリポジトリ${VERSION}-osx-signed/"${SIGNER}"${VERSION}-win-signed/"${SIGNER}"にコミットされる。

そして最後に、ビルドされた成果物に対して、SHA256SUMS.ascが生成され、それにGPG署名される。

*1:Apple IDでログインが必要

*2:7.3GBほどの容量がある