はじめに
『DApps開発入門』という本や色々記事を書いているかるでねです。
今回は、BitcoinのブロックヘッダをEthereum上に載せ、Merkle証明で取引の包含を検証するSPVゲートウェイを定めたERC8002についてまとめていきます!
以下にまとめられているものを解説しながらまとめていきます。
他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。
概要
ERC8002は、Bitcoin上の取引をEthereumで検証するためのISPVGatewayインターフェースを定めた提案です。
ドラフトのためデプロイ先アドレスはプレースホルダのままです。
要点の整理
提案の骨格は、誰でも有効なBitcoinブロックヘッダをオンチェーンに積み上げ、累積仕事量でメインチェーンを追い、Merkle証明で「このtxIdがそのブロックに含まれたか」をcheckTxInclusionに問い合わせられる点にあります。
ウォレットやオフチェーンのヘルパがヘッダを送り、消費側のプロトコルが確認数のしきい値を自分で決めます。
| 項目 | 内容 |
|---|---|
| 正式名称 | Simplified Payment Verification Gateway |
| ステータス | Draft |
| コアコントラクト | シングルトンSPVGateway(ISPVGatewayを実装) |
| 入力 | 誰でも有効なBitcoinブロックヘッダ(80バイト)をaddBlockHeaderで追加 |
| メインチェーン | 累積PoWが最大となる枝として先端を更新 |
| 取引の検証 |
txId・Merkle証明・blockHashなどからcheckTxInclusionで包含を確認 |
Simplified Payment Verification(SPV)
フルノードのように全取引を保持せず、ブロックヘッダとMerkle証明だけで支払の存在を確認する、Bitcoinの軽量クライアント向けの考え方です。
ERC8002は、その検証ロジックをEthereum上のコントラクトとして共有するためのゲートウェイです。
シングルトンとしての狙い
この提案では、Bitcoin上の取引をオンチェーンで検証するシングルトンを導入し、その提出先アドレスは現時点でプレースホルダです。
信頼不要なSPVゲートウェイとして、誰でもBitcoinブロックヘッダを提出でき、メインチェーンを保持しつつMerkle証明で取引の存在を検証できます。
全体像
以下の図は、Bitcoin側からブロックヘッダが提出され、Ethereum上のプロトコルがcheckTxInclusionなどで検証に使う流れを示しています。
動機
背景
提案の動機では、Ethereumのスマートコントラクトによる分散化・信頼不要・プログラマブルな価値の実現に加え、DeFiの再興、RWAやプライバシー系プロトコルなどの成果が挙がる、と書かれています。
一方、Bitcoinはプログラマビリティが極めて限定的です。
そのため信頼不要な範囲では保有と送受信に留まりやすく、EthereumのDeFiにネイティブなBTCを直接組み込みにくい、という課題が出てきます。
想定されるユースケース
この提案は、その制約を埋め合わせる基盤を置くことを狙いとします。
ネイティブBTCを担保にステーブルコインを借りるといったユースケースを既に可能にする、と書いてあります。
さらにBitVMなどの進展と組み合わせると、オーナー不在の二方向ブリッジや、ラップなしのBTC発行をEthereum上で目指せる、といった展望にも触れています。
仕様
提案では、まずビットコインのブロックヘッダ・難易度・検証・Merkle・メインチェーンといった前提を書き、そのうえでEthereum上のシングルトン SPVGateway が実装するISPVGatewayのイベント・関数を定めます。
以下はその順に沿った整理です。
Bitcoinブロックヘッダの構造
各ブロックのヘッダは固定長80バイトのフィールドで構成され、提案の表では以下のように定めています。
| フィールド名 | サイズ | バイト順 | 説明 |
|---|---|---|---|
| Version | 4バイト | リトルエンディアン | ブロックのバージョン番号 |
| Previous Block | 32バイト | natural byte order | このブロックが接続する先のブロックのハッシュ |
| Merkle Root | 32バイト | natural byte order | ブロック内に含まれる取引の指紋(Merkle Treeのルート) |
| Time | 4バイト | リトルエンディアン | Unix時刻のタイムスタンプ |
| Bits | 4バイト | リトルエンディアン | 難易度の目標(target)の圧縮表現 |
| Nonce | 4バイト | リトルエンディアン | マイニングで探索する32ビット値 |
フィールドの並びは上表の順序に従います。
バイト境界は以下のように連なります(合計80バイト)。
バイト順(LEとnatural byte order)
図中の「LE」はリトルエンディアンの略です。Version・Time・Bits・Nonceのように幅が4バイトのフィールドは、いずれも数値として解釈され、そのとき下位バイトから先に並べるのがBitcoinのブロックヘッダの慣習です。
一方、Previous BlockとMerkle Rootはそれぞれ32バイトのハッシュ値です。4バイト整数のようにエンディアンを反転して並べるのではなく、SHA256などが出力したバイト列を先頭からそのままヘッダに載せる扱いになります。表の「natural byte order」は、この「ハッシュの生のバイト順をいじらない」ことを指しています。
難易度調整
BitcoinのPoWは確率的なファイナリティに依拠するため、動的な難易度目標を更新します。
提案では、目標の初期値が0x00000000ffff0000000000000000000000000000000000000000000000000000に設定され、これが最小難易度のしきい値も兼ねる、と書いてあります。
targetは約2016ブロックごとに再計算され、この区間は難易度調整期間と呼ばれます。
そこで期待される期間の長さは、2016ブロック×10分ブロックとし、秒に換算すると1,209,600秒です。
新しいtargetは、現在のtargetに、直前の2016ブロックを採掘するのに実際にかかった時間と、その期待時間(1,209,600秒)の比を掛けた値として求められます。
急激な変動を防ぐため、この比にかかる倍率は最大で4倍、最小で1/4倍にクランプされます。
以上の流れを以下の図にまとめました。
指数や係数の丸め方など、更新の細部は提案に定めています。
ブロックヘッダの検証ルール
有効なヘッダとしてチェーンに採用されるには、Chain Cohesion(チェーンの連結)、Timestamp Rules(時刻のルール)、PoW Constraint(PoWの制約)の3つに合致する必要がある、と提案では整理しています。
その関係を図にすると以下のとおりです。続く番号付きリストで、それぞれのルールの中身を掘り下げます。
3つの検証はいずれも満たさなければ以降の統合処理に進めません。
以下はその中身です。
-
Chain Cohesion
Previous Blockのハッシュは、既存のヘッダ集合の中に存在する有効なブロックを指す必要があります。 -
Timestamp Rules
Timeは直前11ブロックのMedian Time Past(MTP、中央時刻)より大きくなければなりません。
またTimeは、検証ノードのネットワーク調整時刻から見て2時間より未来を示してはなりません。 -
PoW Constraint
ヘッダに対してSHA256を2回適用した結果が、難易度調整から得られる現在のtarget以下である必要があります。
取引のMerkle Treeと包含検証
各ブロックヘッダのMerkle Rootは、そのブロックに含まれる取引のMerkle Treeのルートです。
葉は生の取引バイト列の二重SHA256、隣接ノードがない場合は自分自身とペアにして再帰的にハッシュ化する、という構築方法が提案に書いてあります。
取引の包含を検証するには、Merkle証明とtxIdなどからMerkle Treeのルートを再構築し、ヘッダに保存されたMerkle Rootと一致するか比較する必要があります。Merkleパスとハッシュ方向のビットは、Bitcoinフルノードへの問い合わせで取得できる、と書かれています。
実際のTreeは取引数に応じて深さが変わり、奇数のときは葉が自分自身とペアになる、といった提案上のルールがあります。
葉からルートへハッシュが集約される様子は以下の図のとおりです。
メインチェーンと累積PoW
Bitcoinのメインチェーンは、単に長いチェーンではなく、累積PoWが最大となる枝です。
1ブロックの仕事量はtargetに反比例し、提案では(2**256 - 1) / (target + 1)として表されます。
targetはBitsフィールドから導かれ、先頭1バイトが左シフト量、残り3バイトが実際のtarget値を表す、と書いてあります。
数値のイメージを掴むと分かりやすいので、Bitcoinと同様の圧縮表現として以下のように整理できます。
-
Bitsは4バイト(リトルエンディアン)。これを32ビット整数として読むと、上位1バイトが指数(左シフト量に相当)、下位3バイトが係数(実際のtargetを組み立てる値)を表す、という対応です。 - 例として、ワイヤ上の4バイトが
ff ff 00 1d(LE)のとき、整数としては0x1d00ffffと解釈され、指数0x1d、係数0x00ffffからtarget = 0x00ffff × 2^(8×(0x1d−3))のように256ビットのtargetへ展開される、というイメージです。
チェーン全体の累積仕事量は、チェーン上の各ブロックの仕事量の和であり、累積PoWが最大となる枝に属するブロックがメインチェーン上にあります。
分岐があったときに「長さ」ではなく累積PoWで枝を比べるイメージです。
実際のノードはより多くの分岐を扱いますが、選ぶ基準は「累積PoWが最大の枝」である点がポイントです。
SPVGatewayの初期化に関する必須要件
SPVGatewayは、パーミッションレスな初期化を提供しなければなりません。
この仕組みは、有効なBitcoinブロックヘッダ、対応するブロック高さ、そのブロックまでの累積PoWを、特別な権限なしに提出できることを許可しなければなりません。
誰でも起点となる状態を載せられる、という関係を図示します。
管理者キーでロックするのではなく、誰でも起点データを載せられる設計であることが、この提案の前提です。
ISPVGatewayインターフェース
以下は提案に掲載されているISPVGatewayの各要素です。
シグネチャの直後に、構造体・イベント・関数ごとに表で引数・戻り値(またはフィールド)を示します。
提出系の関数が状態を更新し、参照系の関数とcheckTxInclusionがその状態を読む、という役割分担を以下に示します。
イベントの発火条件は、各関数の節で書いてあるとおりです。
読み取り系は状態を変えず、checkTxInclusionだけが包含検証という重い読み取りを担います。
BlockHeaderData
Bitcoinブロックヘッダをコントラクト内で扱うための構造体です。
提案では、すべてのフィールドをビッグエンディアンに変換して内部表現とし、EVMの効率を優先します(Bitcoinの整数フィールドのリトルエンディアンとは向きが異なります)。
struct BlockHeaderData {
bytes32 prevBlockHash;
bytes32 merkleRoot;
uint32 version;
uint32 time;
uint32 nonce;
bytes4 bits;
}
| フィールド | 型 | 説明 |
|---|---|---|
prevBlockHash |
bytes32 |
前ブロックのハッシュ。 |
merkleRoot |
bytes32 |
ブロック内取引のMerkleルート。 |
version |
uint32 |
ブロックバージョン。 |
time |
uint32 |
タイムスタンプ。 |
nonce |
uint32 |
マイニング用のnonce。 |
bits |
bytes4 |
難易度の圧縮表現。 |
MainchainHeadUpdated
メインチェーンの先端が変わった時に発火するイベントです。
addBlockHeaderやaddBlockHeaderBatchなどで更新が起きる場合に必ずemitする、と定めています。
event MainchainHeadUpdated(
uint64 indexed newMainchainHeight,
bytes32 indexed newMainchainHead
);
| 引数 | 説明 |
|---|---|
newMainchainHeight |
新しいメインチェーン先端の高さ。 |
newMainchainHead |
新しいメインチェーン先端のブロックハッシュ。 |
BlockHeaderAdded
新しいブロックヘッダがコントラクト上の状態に追加された時に発火するイベントです。
addBlockHeaderやaddBlockHeaderBatchなどで追加が発生した場合に必ずemitする、と定めています。
event BlockHeaderAdded(uint64 indexed blockHeight, bytes32 blockHash);
| 引数 | 説明 |
|---|---|
blockHeight |
追加されたブロックの高さ。 |
blockHash |
追加されたブロックのハッシュ。 |
addBlockHeader
生の80バイトのブロックヘッダを1つ追加する関数です。
追加前に検証が行われます。
function addBlockHeader(bytes calldata blockHeaderRaw) external
| 区分 | 名前 | 型 | 説明 |
|---|---|---|---|
| 引数 | blockHeaderRaw |
bytes calldata |
生のブロックヘッダバイト列(80バイトであること)。 |
| 戻り値 | — | — | 提案のインターフェース上、戻り値の明示はありません。 |
提案では、addBlockHeaderは以下の処理を必ず実行しなければなりません(提案本文の列挙順に合わせています)。
-
PoW Constraint: ヘッダに対してSHA256を2回適用した結果が、難易度調整から得られる現在の
target以下であることを検証する(「ブロックヘッダの検証ルール」の3. PoW Constraintと同じ内容)。 - 提出された生ヘッダの長さが80バイトであることを検証する。
- 「ブロックヘッダの検証ルール」に書かれたChain CohesionとTimestamp Rulesを検証する。
- 新しいヘッダを既知のチェーンに統合する際に、累積PoWを計算し、「メインチェーンと累積PoW」に従ってリオーガナイゼーションを含めて管理する。
- 追加に成功したら
BlockHeaderAddedをemitする。 - メインチェーンが更新されたら
MainchainHeadUpdatedをemitする。
上記の手順の流れを図にしたものです。
検証のどこかで失敗した場合は、以降の統合処理に進みません。
先端が変わらない追従だけならMainchainHeadUpdatedは不要、という整理です。
addBlockHeaderBatch
複数のブロックヘッダを順に追加するオプションの関数です。
各ヘッダは検証と追加が順番に行われます。
function addBlockHeaderBatch(bytes[] calldata blockHeaderRawArray) external
| 区分 | 名前 | 型 | 説明 |
|---|---|---|---|
| 引数 | blockHeaderRawArray |
bytes[] calldata |
生ヘッダの配列。 |
| 戻り値 | — | — | 提案のインターフェース上、戻り値の明示はありません。 |
提案のRationale(設計根拠)では、11ブロックを超えるバッチでは、MTP(中央時刻)の計算にcalldata上のタイムスタンプを使えるため、ストレージ読み込みが減り、ガスとトランザクションコストを大きく下げられる、と書いてあります。
checkTxInclusion
指定したtxIdが、指定したブロックのMerkle Treeに含まれ、かつそのブロックがメインチェーン上にあり、かつ十分な確認数があるかを検査するview関数です。
function checkTxInclusion(
bytes32[] memory merkleProof,
bytes32 blockHash,
bytes32 txId,
uint256 txIndex,
uint256 minConfirmationsCount
) external view returns (bool)
| 名前 | 型 | 説明 |
|---|---|---|
merkleProof |
bytes32[] memory |
Merkleルートに到達するまでに使うハッシュの配列。 |
blockHash |
bytes32 |
検証対象のブロックのハッシュ。 |
txId |
bytes32 |
検証したい取引ID。 |
txIndex |
uint256 |
ブロック内のMerkle Treeにおける取引のインデックス。 |
minConfirmationsCount |
uint256 |
要求する最小確認数。 |
| 戻り値の型 | 説明 |
|---|---|
bool |
条件を満たせばtrue、満たせなければfalse。 |
提案では、checkTxInclusionは以下の手順を必ず実行しなければなりません。
-
blockHashがメインチェーンの一部であること、およびそのブロックの確認数がminConfirmationsCount以上であることを確認する。いずれかに失敗したらfalseを返す。 -
merkleProof、txId、txIndexを用いてMerkleルートを計算する。 - 計算したMerkleルートを、
blockHashで識別されるブロックヘッダに保存されたMerkle Rootと比較する。 - 一致すれば
true、一致しなければfalseを返す。
以下は上記の流れを図示したものです。
図の分岐は、提案が必須とする順序と対応しています。
まずメインチェーン上の位置と確認数で早期に失敗し、その後Merkle再計算でルートの一致を見ます。
参照系のview関数
状態を読むだけのview関数は、メインチェーンの先端や各ブロックのメタデータを取得するためのものです。
いずれも状態を変更しません。
メインチェーン先端
getMainchainHeadは、累積PoWが最大となる枝の先端ブロックのハッシュを返します。
getMainchainHeightは、その高さを返します。
getBlockHashは、指定した高さにあるメインチェーン上のブロックのハッシュを返します。
ブロック単体の参照
getBlockHeaderは、保存済みのBlockHeaderDataを返します。
getBlockHeightは、blockHashから高さを引きます。
getBlockMerkleRootは、ヘッダに保存されたMerkleルートを返します。
blockExistsは、コントラクトがそのblockHashを保持しているかを返します。
getBlockStatus
getBlockStatusは、メインチェーン上かどうか(isInMainchain)と、そのブロックの上に積まれたブロック数に相当する確認数(confirmationsCount)を一度に返します。
checkTxInclusionを呼ぶ前に、同じ前提を自前で確認したいときの入口になります。
以下の表に、主な関数と返す情報をまとめました。
| 関数 | 返す情報 |
|---|---|
getMainchainHead |
累積PoWが最大のメインチェーン先端のブロックハッシュ |
getMainchainHeight |
累積PoWが最大のメインチェーンにおける先端のブロック高さ |
getBlockHeader |
指定blockHashに対応するBlockHeaderData
|
getBlockStatus |
メインチェーン上か(isInMainchain)と、その上に積まれたブロック数に相当する確認数 |
getBlockMerkleRoot |
ヘッダに保存されたMerkleルート |
getBlockHeight |
既知のblockHashのブロック高さ |
getBlockHash |
指定高さのメインチェーン上のブロックハッシュ |
blockExists |
ストレージに当該blockHashが登録されているか |
シグネチャの一覧は提案本文のインターフェース定義に従います。
個別の引数・戻り値の意味は上表と対応しています。
function getMainchainHead() external view returns (bytes32);
function getMainchainHeight() external view returns (uint64);
function getBlockHeader(bytes32 blockHash) external view returns (BlockHeaderData memory);
function getBlockStatus(bytes32 blockHash) external view returns (bool isInMainchain, uint64 confirmationsCount);
function getBlockMerkleRoot(bytes32 blockHash) external view returns (bytes32);
function getBlockHeight(bytes32 blockHash) external view returns (uint64);
function getBlockHash(uint64 blockHeight) external view returns (bytes32);
function blockExists(bytes32 blockHash) external view returns (bool);
参照実装
提案は、同じEIPページにSPVGateway.solの参照実装と、難易度のBitsとtargetの相互変換などを扱うTargetsHelper.solへのリンクを載せています。
依存するライブラリのバージョンも提案に書かれています。
設計根拠(Rationale)
EIP/ERCでは、採用した設計の理由やトレードオフをまとめた節を Rationale(ラシオナーレ)と呼びます。ここではその内容の要約です。
初期化の比較
設計時に、初期化の取り方として以下の3案が並んでいます。
以下の図は、この節で並べられる3案の位置づけと、提案が②を採用形としている点をまとめたものです。
-
Bitcoinジェネシスをハードコードする
デプロイの起点は最も信頼不要だが、フル同期のコストが大きく、提案の例ではガス単価1 gweiのとき約100 ETH規模のフル同期に相当する、と試算しています。 -
任意の高さからのブートストラップ
初期の累積仕事量と高さを信頼する必要があるが、ライトクライアントの起動で一般的な方法であり、コミュニティ検証済みチェックポイントなどでオフチェーン補強が可能。 -
歴史の正しさをゼロ知識証明で保証する
高度だが、特定ブロックまでの歴史を一括で証明できる。
現行の提案では、初期化の採用形は2です。
エンディアン
生ヘッダを提出した後、BlockHeaderDataの各フィールドはビッグエンディアンに正規化されます。
EVMの効率を優先するためであり、Bitcoinの整数のリトルエンディアンとは逆の扱いになります。
ワイヤ上のBitcoinヘッダと、コントラクト内の構造体の向きの違いを図示します。
ハッシュ系のbytes32は向きの問題が出にくく、主にversionやtimeなどの整数フィールドでエンディアン変換の話になります。
ファイナリティ
SPVGatewayは、消費側プロトコルが必要とする確定の基準を決める「ファイナリティのルール」を持ちません。
しきい値は利用側が個別に定めます。
バッチ追加とガス
オプションのaddBlockHeaderBatchはガス最適化のために含まれ、11ブロック超のバッチではMTP計算にcalldataのタイムスタンプを使えるため、ストレージ読み込みとコストを大きく下げられる、と書いてあります。
複数ヘッダを順に処理するイメージです。各要素は個別に検証・追加されます。
長い同期を一度のトランザクションにまとめる用途を想定した関数であり、中身は単一追加の繰り返しです。
セキュリティ
BitcoinのPoWへの依存
ゲートウェイの安全性はBitcoinのPoWコンセンサスに直結します。
51%攻撃が成功すると、攻撃者が提出する不正なヘッダが受け入れられ、状態が破綻する可能性があります。
2時間先の時刻チェックの省略
ノード実装ではネットワーク調整時刻に対する「2時間先まで許容しない」チェックがありますが、SPVGatewayではネットワーク調整時刻が取れないため、このチェックは実装しません。
確率的ファイナリティとリオーグ
Bitcoinは確率的なファイナリティであり、SPVGatewayは任意の深さのリオーガナイゼーションに対応するよう設計されるべきですが、リオーグの発生を防ぐことはできません。
ブロックに含まれた取引が恒久的に確定するとは限らないため、利用するdAppとプロトコルは、自前のセキュリティ方針で十分な確認数を決めなければなりません。
初期状態の信頼点
addBlockHeaderはパーミッションレスで各ヘッダを暗号学的に検証しますが、開始時のブロックヘッダ・高さ・累積PoWは信頼の起点になります。
設計上の柔軟なブートストラップを許容する一方、初期状態の検証責任はコミュニティと、当該デプロイを使うdAppの選択にあります。
リファレンス実装
Solidityの参考実装はethereum/ERCsリポジトリに含まれています。
TargetsHelperは、Bitcoinの難易度targetとBitsの相互変換や、難易度調整期間における新しいtargetの計算などを提供する補助ライブラリです。
依存関係として、提案では@openzeppelin/contracts v5.2.0、@solarity/solidity-lib v3.2.0、solady v0.1.23が列挙されています。
議論とステータス
Ethereum Magicians上のスレッドは以下のURLです。
本提案はドラフトであり、デプロイアドレス・テストケース・デプロイ手順などは未確定のプレースホルダが残っています。
互換性について提案本文に「後方互換」とある場合でも、何に対する互換性か(例: 既存のEthereumアップグレードや他規格との関係)は原文の該当節で確認してください。
本番利用の前に、常に最新の原文と実装を確認してください。
最後に
今回は「ERC8002」についてまとめてきました。
Bitcoinヘッダのバイトレイアウトから難易度調整、Chain CohesionとMTPとPoWの3条件、累積PoWでのメインチェーン選定までを押さえたうえで、addBlockHeaderの必須ステップとcheckTxInclusionの分岐、Rationaleの初期化トレードオフ、セキュリティ上の前提(PoW依存・2時間ルール省略・ブートストラップの信頼点)までを、提案の順に整理しました。
参照系のviewは、メインチェーン先端・ブロック単体・getBlockStatusのように小見出しでまとめ、表とシグネチャでAPI全体の地図を掴めるようにしています。
他でも色々記事を書いているのでぜひよろしければ読んでいってください!




