
CTransaction::GetHash() とCBlock::GetHash() は、同じ種類のオブジェクトをハッシュしていない。前者はトランザクションオブジェクトを直列化する。CBlock::GetHash() がハッシュするのは、メモリー上のnVersionからnNonceまでで、トランザクションの並びはそこに入らない。トランザクションの中身は、hashMerkleRootを通してヘッダーに入る。
この違いで、このあとのデータの通り道が、すべて決まる。同じ直列化フレームワークがネットワークメッセージ、ブロックファイル、オブジェクト識別子、署名ダイジェストに使われる。ただし、どんなバイトの並びになるかは、呼び出す側がどの用途を選ぶか、あるいは、呼び出す前にオブジェクトにどんな手を加えるかで決まる。v0.1には、「直列化されたトランザクション」と言えば一つに決まる、どこでも同じバイトの並び、というものはない。
1 直列化の処理は一つ、使われる場面は主に三つ
src/serialize.hは、3つの主なコンテキストを宣言する:
| コンテキスト | v0.1での役割 |
|---|---|
SER_NETWORK | P2Pメッセージに載せるオブジェクトを直列化する。 |
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は保存レコードへつながる。ブロック識別子は別のヘッダー範囲を通り、マークルルートがトランザクションハッシュをヘッダーへ運ぶ。
2 トランザクションハッシュはv0.1のオブジェクト全体を対象にする
src/main.hのCTransactionには、トップレベルの直列化フィールドが次の順序で並ぶ:
nVersionvinvoutnLockTime
各入力はprevout、scriptSig、nSequenceを含む。各出力はnValueとscriptPubKeyを含む。CTransaction::GetHash() はSerializeHash(*this) を呼び、SerializeHashはSER_GETHASHを持つCDataStreamを作り、オブジェクトを直列化して、ソースにある二重SHA-256のHash関数にかける。
v0.1には、後のSegWitで入った証人(ウィットネス)のフィールドがなく、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つある:
CTransaction::GetHash()に使う元の直列化トランザクション。SignatureHashに使う変更済みコピー。- ネットワークまたはディスクのコンテキストで直列化されたトランザクション。
この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は逆の処理を行い、読み戻したあとでヘッダーのハッシュを確かめる。ファイルに書くときは、ブロックを別のデータで包むが、それでブロックの識別子が変わるわけではない。このマジックナンバーとサイズによる枠組みは、v0.1で生まれ、今のblk*.dat形式まで変わらずに続いている。その流れはストレージ設計のページでたどった。ただしBitcoin Core 28.0からは、ディスクのファイルは、既定でXORをかけて保存される。
トランザクションデータベースも同じ区別を使う。CTxDB::AddTxIndexはtx.GetHash() をキーとして計算し、ディスク位置と出力数を持つCTxIndexを保存する:
トランザクションオブジェクト
→ CTransaction::GetHash()
→ Berkeley DB キー: ("tx", トランザクションハッシュ)
→ CTxIndex: ディスク位置 + 出力数
→ ブロックファイル内の直列化トランザクション
ハッシュはトランザクションを識別し、インデックスは直列化バイト列の位置を示す。ハッシュとインデックスは関わり合うが、同じものではない。
7 境界を一つの表にまとめる
| 経路 | コンテキストまたは処理 | 直接の入力または結果 |
|---|---|---|
| トランザクション識別子 | SER_GETHASHによるSerializeHash | v0.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が同じソース配置やすべての内部呼び出し経路を保持していることまでは示さない。後世のトランザクション形式、証人処理、チェーン状態保存には、それぞれのソースとバージョンの境界が必要になる。





