ビットコイン・インスティテュート

ビットコインv0.1の​直列化境界 — オリジナルソースに​おける​トランザクション・ブロック・署名ハッシュ

ビットコインv0.1の直列化経路を抽象化した技術図。トランザクションのバイト列、マークルルートの分岐、ブロックヘッダーの境界、署名ダイジェストの経路、ネットワークとディスクのフレーミングを示す。

CTransaction::GetHash() とCBlock::GetHash() は、同じ種類のオブジェクトをハッシュしていない。前者はトランザクションオブジェクトを直列化する。後者がハッシュするのはnVersionからnNonceまでのメモリー範囲であり、トランザクションベクターはそこに含まれない。その内容はhashMerkleRootを通じてヘッダーに入る。

この分岐が後続するすべての経路を決める。同じ直列化フレームワークがネットワークメッセージ、ブロックファイル、オブジェクト識別子、署名ダイジェストに使われる。ただし、呼び出し側がコンテキストを選ぶか、先にオブジェクトを変換する。v0.1に「直列化されたトランザクション」という普遍的なバイト列はない。

1. 1つの直列化器と3つの主要コンテキスト

src/serialize.hは、3つの主要な処理を宣言する:

コンテキストv0.1での役割
SER_NETWORKP2Pメッセージに載せるオブジェクトを直列化する。
SER_DISKブロックファイルやデータベースレコードに保存するオブジェクトを直列化する。
SER_GETHASHオブジェクト識別子や署名ハッシュの入力に使うバイトを直列化する。

SER_SKIPSIGとSER_BLOCKHEADERONLYは、別の直列化世界ではなく修飾子である。IMPLEMENT_SERIALIZEマクロは、1つのフィールド記述からサイズ計算、書き込み、読み込みの3操作を展開する。ストリームはnTypeとnVersionをその記述へ渡すため、オブジェクトはフィールドリストを別々に持たずにコンテキストごとの選択ができる。

仕組みは小さいが、結果は単純ではない。ネットワークやディスクのレコードに存在するフィールドを、ハッシュ入力から除外できる。呼び出し側のコンテキストが、バイト列の意味の一部になる。

実装範囲はv0.1のソースで固定されている。ストリームのコンテキストはsrc/serialize.h:28–79、トランザクションのフィールドとハッシュはsrc/main.h:193–395、ブロックヘッダー・マークルルート・ブロックハッシュはsrc/main.h:805–884、署名検査とSignatureHashはsrc/script.cpp:692–712, 818–901、ネットワークメッセージのフレーミングはsrc/net.h:434–494, 565–675、ブロックファイルのフレーミングはsrc/main.h:919–959、トランザクションインデックスはsrc/db.cpp:186–201、ハッシュ関数はsrc/util.h:363–390にある。

バイト列の経路はストリーム種別で分かれる。SER_GETHASHはトランザクション識別子と署名ダイジェストへ、SER_NETWORKはピア間ペイロードへ、SER_DISKは保存レコードへつながる。ブロック識別子は別のヘッダー範囲を通り、マークルルートがトランザクションハッシュをヘッダーへ運ぶ。

SER_GETHASH

SER_GETHASH

SER_NETWORK

SER_DISK

トランザクション

ハッシュ

ヘッダー範囲

オブジェクトバイト列

変更済み

トランザクション

ディスク位置

CDataStream

nType + nVersion

CTransaction::GetHash()

SignatureHash()

CNode::PushMessage()

CBlock::WriteToDisk()

CBlock ヘッダー

nVersion ... nNonce

BuildMerkleTree()

hashMerkleRoot

二重 SHA-256

CTxDB::AddTxIndex()

2. トランザクションハッシュはv0.1のオブジェクト全体を対象にする

src/main.hのCTransactionには、トップレベルの直列化フィールドが次の順序で並ぶ:

  1. nVersion
  2. vin
  3. vout
  4. nLockTime

各入力はprevout、scriptSig、nSequenceを含む。各出力はnValueとscriptPubKeyを含む。CTransaction::GetHash() はSerializeHash(*this) を呼び、SerializeHashはSER_GETHASHを持つCDataStreamを作り、オブジェクトを直列化して、ソースの二重SHA-256 Hash関数を適用する。

v0.1には証人フィールドがなく、txidとwtxidを分ける経路もない。v0.1の入力直列化はscriptSigを含み、SER_GETHASHでそれを除外しないため、トランザクションハッシュはトランザクションオブジェクトに存在する署名バイト列を含む。これはこの実装についての記述であり、後世のすべてのビットコイン・トランザクション識別子に対する一般論ではない。

トランザクション設計は、入力と出力が支払いシステムで何を意味するかを扱う。より狭い問題は、そのオブジェクトの識別子になるバイトの境界である。

3. ブロックハッシュはヘッダーで止まる

CBlockは、まずヘッダーを宣言する:

nVersion、hashPrevBlock、hashMerkleRoot、nTime、nBits、nNonce。

その後ろに、ネットワークとディスクのデータであるvtxトランザクションベクターが続く。シリアライザーはnTypeにSER_GETHASHまたはSER_BLOCKHEADERONLYが含まれない場合だけvtxを書く。CBlock::GetHash() はHash(BEGIN(nVersion), END(nNonce)) を呼ぶため、直接のハッシュ入力は完全なブロックオブジェクトではなくヘッダー範囲である。

欠けている接続を作るのがBuildMerkleTree() だ。各トランザクションのtx.GetHash() から始めてハッシュを対にし、最後のルートを返す。トランザクションが変わればhashMerkleRootが変わり、そのヘッダーフィールドが変わればブロックハッシュも変わる。トランザクションリストはブロックハッシュのバイト範囲に直接入らず、ヘッダーを通じて間接的にコミットされる。

ブロック・チェーン設計は、このルートをチェーンのリンクとプルーフ・オブ・ワークへつなげる。その手前の段階では、マークルルートがトランザクションデータとブロック識別子の間の境界である。

4. 署名ダイジェストは変換済みトランザクションである

OP_CHECKSIGはtxTo.GetHash() を直接求めない。src/script.cppのインタープリターは、直近のOP_CODESEPARATORから始まるスクリプトを取り出し、検査対象の署名を除去して、そのscriptCodeをCheckSigに渡す。

続いてSignatureHashはトランザクションをコピーし、ハッシュ前にコピーを変更する:

  • すべての入力のscriptSigを空にする。
  • 現在の入力に選択したscriptCodeを設定する。
  • SIGHASH_NONEはすべての出力を削除し、他の入力のシーケンスをゼロにする。
  • SIGHASH_SINGLEは現在の入力インデックスまでの出力を残し、それより前をnullにする。
  • SIGHASH_ANYONECANPAYは現在の入力だけを残す。
  • 変更済みトランザクションをSER_GETHASHで直列化し、nHashTypeを付加して二重ハッシュする。

署名は、意図的に作られたトランザクションの見え方にコミットする。この同じSignatureHashの仕組みを、暗号設計ページはECDSA・シュノアによる署名・検証というより広い枠組みの中に位置づけている。本稿はこの一関数のバイト単位にとどまる。区別すべきバイト列は3つある:

  1. CTransaction::GetHash() に使う元の直列化トランザクション。
  2. SignatureHashに使う変更済みコピー。
  3. ネットワークまたはディスクのコンテキストで直列化されたトランザクション。

この3つをすべて「トランザクションのバイト」と呼ぶと、署名の有効性を決める規則が消える。

5. ネットワーク転送はオブジェクトの直列化器を再利用する

CNodeのコンストラクターはvSendとvRecvの両方をSER_NETWORKに設定する。BeginMessageは送信ストリームへCMessageHeaderを書く。続いてPushMessageがoperator<< で引数を直列化し、EndMessageがヘッダーへペイロード長を書き戻す。

経路は次のとおりである:

CNode::PushMessage(command, object)
    → CMessageHeader(command, size)
    → CDataStream(SER_NETWORK) << object
    → 直列化されたペイロード

メッセージヘッダーは転送用のフレーミングであり、CTransaction::GetHash() やCBlock::GetHash() の入力ではない。トランザクションはネットワーク転送と別の経路で同じオブジェクト直列化器を使いながら、メッセージコマンドやピアごとのフレーミングから独立した識別子を持つ。

6. ディスク保存はフレーミングと位置を加える

CBlock::WriteToDiskはブロックファイルを取得し、必要ならSER_BLOCKHEADERONLYを設定し、直列化サイズを計算し、ネットワークマジックとサイズプレフィックスを書き、その後にブロックを書き込む。ReadFromDiskは逆の処理を行い、非直列化の後でヘッダーハッシュを検査する。ファイルレコードはオブジェクトを包むが、別のブロック識別子を作るわけではない。ストレージ設計のページは、このマジックナンバーとサイズによる枠組みを現行のblk*.dat形式にまで辿っている。v0.1に起源を持ち、以来変わっていない構造である。

トランザクションデータベースも同じ区別を使う。CTxDB::AddTxIndexはtx.GetHash() をキーとして計算し、ディスク位置と出力数を持つCTxIndexを保存する:

トランザクションオブジェクト
    → CTransaction::GetHash()
    → Berkeley DB キー: ("tx", トランザクションハッシュ)
    → CTxIndex: ディスク位置 + 出力数
    → ブロックファイル内の直列化トランザクション

ハッシュはトランザクションを識別し、インデックスは直列化バイト列の位置を示す。この2つは関連する処理だが、同じものではない。

7. 境界を一つの表にまとめる

経路コンテキストまたは処理直接の入力または結果
トランザクション識別子SER_GETHASHによるSerializeHashv0.1のscriptSigを含むCTransaction全体の直列化
ブロック識別子CBlock::GetHash()nVersionからnNonceまでのヘッダー範囲
マークルコミットメントBuildMerkleTree()各tx.GetHash() から始まるペアごとのハッシュ
署名ダイジェストSignatureHash()変更済みトランザクション + nHashType
ネットワークペイロードCDataStream(SER_NETWORK)CMessageHeader内のオブジェクトペイロード
ブロックファイルレコードSER_DISK、必要に応じてSER_BLOCKHEADERONLYフレーミングされたオブジェクトバイトとディスク位置
トランザクションインデックスCTxDB::AddTxIndex()("tx", tx.GetHash()) のデータベースキーとCTxIndex

サトシのコード分析はコーディングスタイル、コミットパターン、コードベースの進化を扱う。前者が誰がいつコードを変更したかを追うのに対し、こちらでは初期実装が動作するときのフィールドと呼び出し境界を追う。

8. v0.1のソースが確定させること

ソースから曖昧さなく読める実装上の事実は4つある:

  • トランザクションの同一性は、scriptSigを含む完全なレガシートランザクションの直列化に結びつく。
  • ブロックの同一性はヘッダーに結びつき、トランザクション内容はhashMerkleRootを通じて入る。
  • 署名検証はトランザクション識別子を直接使わず、変換済みトランザクションとハッシュタイプ整数を使う。
  • ネットワーク転送、ディスク保存、データベースインデックスは直列化プリミティブを再利用するが、識別子自体は再定義しない。

これは固定したv0.1ソースについての事実である。現行Bitcoin Coreが同じソース配置やすべての内部呼び出し経路を保持していることまでは示さない。後世のトランザクション形式、証人処理、チェーン状態保存には、それぞれのソースとバージョンの境界が必要になる。

参照元の外部ソース

https://github.com/trottier/original-bitcoin/tree/4184ab26345d19e87045ce7d9291e60e7d36e096

その他の外部ソース