1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[ERC8179] Blobを複数プロトコルで分けて使うBlob Space Segmentsの仕組みを理解しよう!

1
Posted at

はじめに

初めまして。
『DApps開発入門』という本や色々記事を書いているかるでねです。

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!

今回は「EIP4844のBlobをFE単位のセグメントに分けて、複数プロトコルが同じBlob空間を共有できるようにするERC8179」についてまとめていきます。

ERC8179は、Blobのどの範囲を誰がどの用途で使ったかを、BlobSegmentDeclared イベントとして宣言するための提案です。
コア仕様はストレージへ書き込まず、イベントログだけを正本にするため、Blob共有の境界を安いガスで公開できます。

以下にまとめられているものを解説しながらまとめていきます。

概要

ERC8179は、EIP4844のBlobを「FEの連続範囲」として区切り、その区間をオンチェーンイベントで宣言するためのインターフェースです。
Blobは4096個のFEで構成され、1つのFEは32バイトです。
ただし、EIP4844の標準的なエンコードでは高位1バイトを0にして低位31バイトにデータを入れるため、1Blobで使えるデータ量はおよそ126,976バイトです。

EIP4844は、ロールアップなどが大量データを安く投稿するために、通常のCalldataとは別のBlobデータ領域を導入した提案です。
BlobそのものはEVMから直接読めませんが、トランザクションに含まれるBlobの versionedHashBLOBHASH オペコードで取得できます。

EIP4844については以下の記事を参考にしてください。

ERC8179の中心は、declareBlobSegment を呼び出して、Blob内の [startFE, endFE) という半開区間を宣言することです。
半開区間では startFE を含み、endFE を含みません。
例えば [0, 1500)[1500, 3000) は、境界のFEを重複させずに並べられます。

Blobを複数プロトコルで分ける全体像は以下です。

ERC8179の全体像

1つのBlobを、L2バッチ、ソーシャルメッセージ、アプリ別データのような複数用途へ分けられます。
各参加者は同じ BlobSegmentDeclared イベントフォーマットで、自分が使うFE範囲と contentTag を公開します。

動機

EIP4844のBlobは128KiBの固定サイズですが、すべてのロールアップが毎回Blobを埋め切るわけではありません。
提案で参照されている調査では、26個のロールアップを6か月分析し、大きいロールアップはBlob利用率が高く、小さいロールアップは5%未満に留まる傾向が示されています。
また、Blob共有によって80%から99%のデータ可用性コスト削減を目指せるとされています。

Blobの未使用FEは、データ可用性としては支払済みの空間です。
小さいプロトコルが空いているFEを使えるなら、既存のBlob送信者は費用の一部を回収でき、別のプロトコルは自分だけでBlobを出すより低コストでデータを投稿できます。
ただし、どのFE範囲を誰が使ったかを共通フォーマットで示す方法がなければ、インデクサー、検証コントラクト、分析ツールがプロトコルごとの独自実装に分かれます。

ERC8179は、このうち「区間の宣言」だけを標準化します。
Blobの組み立て方、参加者間の料金精算、セグメントの交渉方法は対象外です。
対象を狭くすることで、既存のBlob送信処理に追加のコントラクト呼び出しを足すだけで採用できます。

仕様

ERC8179は、EIP4844上のBlobを前提にし、BLOBHASH オペコードで得られる versionedHash とFE範囲をイベントへ結びつけます。
コアインターフェースはストレージを使わず、宣言の記録はイベントログだけです。

用語

仕様で使う主な用語は以下です。

用語 内容
FE EIP4844のBlobを構成するフィールド要素。1Blobあたり4096個あり、1個は32バイトです。
セグメント Blob内の連続したFE範囲です。[startFE, endFE) の半開区間で表します。
versionedHash EIP4844のBlobコミットメントに基づくハッシュです。BLOBHASH オペコードで取得します。
contentTag セグメントを使うプロトコルやデータ種類を表す bytes32 の識別子です。
宣言者 declareBlobSegment を呼び出す msg.sender です。

contentTag は所有権ではなく、データ種類を識別するラベルです。
そのため、特定のプロトコル名を名乗る宣言が本物かどうかは、宣言者アドレスやプロトコル側の公開ポリシーと合わせて判断します。

IERC_BSS

ERC8179に準拠するコントラクトは、IERC_BSS インターフェースを実装します。

interface IERC_BSS {
    event BlobSegmentDeclared(
        bytes32 indexed versionedHash,
        address indexed declarer,
        uint16 startFE,
        uint16 endFE,
        bytes32 indexed contentTag
    );

    error InvalidSegment(uint16 startFE, uint16 endFE);

    error NoBlobAtIndex(uint256 blobIndex);

    function declareBlobSegment(
        uint256 blobIndex,
        uint16 startFE,
        uint16 endFE,
        bytes32 contentTag
    ) external returns (bytes32 versionedHash);
}

このインターフェースは、1つの関数、1つのイベント、2つのエラーで構成されます。
versionedHashdeclarercontentTag はイベントのインデックス付き引数で、ログ検索の主な軸になります。
一方、startFEendFE は範囲を示す値であり、イベントデータとして記録されます。

declareBlobSegment

function declareBlobSegment(
    uint256 blobIndex,
    uint16 startFE,
    uint16 endFE,
    bytes32 contentTag
) external returns (bytes32 versionedHash);

Blob内のFE範囲を宣言し、対象Blobの versionedHash を返します。

  • 引数
引数 内容
blobIndex 現在のトランザクションに含まれるBlobの0始まりインデックス
startFE セグメントの開始FE。範囲に含まれます
endFE セグメントの終了FE。範囲に含まれません
contentTag プロトコルやデータ種類を表す bytes32 識別子
  • 戻り値
戻り値 内容
versionedHash BLOBHASH(blobIndex) で取得したBlobの versionedHash

処理順は以下です。

declareBlobSegmentの処理フロー

まず startFE < endFEendFE <= 4096 を満たすか確認します。
条件を満たさない時は InvalidSegment(startFE, endFE) でrevertします。
次に、BLOBHASH(blobIndex) でBlobの versionedHash を取得します。
取得結果が bytes32(0) の時は、そのインデックスにBlobが存在しないため NoBlobAtIndex(blobIndex) でrevertします。

検証に通ると、BlobSegmentDeclared イベントを発行し、versionedHash を返します。
コア実装はストレージへ書き込みません。
同じ引数で2回呼び出されても重複排除せず、2つのイベントを発行します。

BlobSegmentDeclaredイベント

event BlobSegmentDeclared(
    bytes32 indexed versionedHash,
    address indexed declarer,
    uint16 startFE,
    uint16 endFE,
    bytes32 indexed contentTag
);

セグメント宣言をイベントログへ記録します。

フィールド 内容
versionedHash 宣言対象Blobの versionedHash
declarer 宣言者アドレス
startFE セグメントの開始FE
endFE セグメントの終了FE
contentTag プロトコルやデータ種類を表す識別子

イベントログは、インデクサーや検証コントラクトの起点になります。
ただし、イベントだけではBlob内のデータ内容までは保証しません。
実際のデータを使う側は、Blobデータ、KZG証明、プロトコル固有のエンコード規則を使って確認します。

InvalidSegmentエラー

error InvalidSegment(uint16 startFE, uint16 endFE);

セグメント範囲が無効な時にrevertします。
startFE >= endFE または endFE > 4096 の時に発生します。

NoBlobAtIndexエラー

error NoBlobAtIndex(uint256 blobIndex);

BLOBHASH(blobIndex)bytes32(0) を返した時にrevertします。
指定された blobIndex にBlobが存在しないことを示します。

blobIndex には上限を固定しません。
Blob数の上限は今後のEIPで増える余地があり、存在しないインデックスは BLOBHASH のゼロ値で判定できます。

contentTag

contentTag は、keccak256("protocol.version") で生成することが推奨されています。
例えば、以下のような値です。

用途 contentTag
ソーシャルメッセージ v4 keccak256("social-blobs.v4")
Optimismバッチ keccak256("optimism.bedrock")
Celestia名前空間 keccak256("celestia.namespace")
汎用ロールアップデータ keccak256("rollup.generic")

bytes32(0)contentTag は許可されていますが、推奨されません。
ゼロ値は keccak256 の出力として現実的に作れず、インデクサーが未設定値の目印として扱う設計と衝突します。

Blob全体の宣言

Blob全体を使うプロトコルは、startFE = 0endFE = 4096 で宣言します。

bss.declareBlobSegment(0, 0, 4096, keccak256("myprotocol.v1"));

この宣言は、Blobを共有しない既存の使い方と意味が一致します。
Blob全体を使うプロトコルでも、同じイベントフォーマットを出せるため、インデクサーや分析ツールは部分利用と全体利用を同じフォーマットで扱えます。

Queryable拡張

オンチェーンで宣言済みセグメントを問い合わせたい用途向けに、任意拡張として IERC_BSS_Queryable が定義されています。

interface IERC_BSS_Queryable is IERC_BSS {
    struct BlobSegment {
        address declarer;
        uint16 startFE;
        uint16 endFE;
        bytes32 contentTag;
    }

    function getSegments(bytes32 versionedHash, uint256 offset, uint256 limit)
        external
        view
        returns (BlobSegment[] memory segments, uint256 nextOffset);

    function segmentCount(bytes32 versionedHash) external view returns (uint256);
}

この拡張は、versionedHash ごとのセグメント一覧をストレージへ保存します。
そのため、コアインターフェースより大きいガスコストを受け入れる必要があります。
読み出し時に無制限配列を返すとガスやメモリの問題が出るため、offsetlimit によるページングが前提です。

Batch拡張

複数のセグメントを1回の呼び出しで宣言する用途向けに、任意拡張として IERC_BSS_Batch が定義されています。

interface IERC_BSS_Batch is IERC_BSS {
    struct BlobSegmentParams {
        uint256 blobIndex;
        uint16 startFE;
        uint16 endFE;
        bytes32 contentTag;
    }

    function declareBlobSegments(BlobSegmentParams[] calldata segments)
        external
        returns (bytes32[] memory versionedHashes);
}

同じBlobに対する複数区間だけでなく、異なる blobIndex の区間も1回で宣言できます。
ただし、どれか1つのセグメントが無効なら、呼び出し全体がrevertします。
これにより、L2バッチとソーシャルメッセージのような複数宣言を同じトランザクション内でまとめて成立させられます。

利用例

2つのプロトコルが1つのBlobを共有する例では、ロールアップバッチが前半を使い、ソーシャルメッセージが後半を使います。

optimismBatcher.submitBatch(...);
bss.declareBlobSegment(0, 0, 2000, keccak256("optimism.bedrock"));
bss.declareBlobSegment(0, 2000, 4096, keccak256("social-blobs.v4"));

この時、BlobのFE範囲は以下のように分かれます。

FE範囲 内容 使用可能データ量の目安
[0, 2000) Optimismロールアップバッチ 62,000バイト
[2000, 4096) ソーシャルメッセージ 64,976バイト

3つのプロトコルが同じBlobを埋める例もあります。

baseBatcher.submitBatch(...);
bss.declareBlobSegment(0, 0, 1500, keccak256("base.bedrock"));
bss.declareBlobSegment(0, 1500, 3000, keccak256("social-blobs.v4"));
bss.declareBlobSegment(0, 3000, 4096, keccak256("celestia.namespace"));

この時、インデクサーは contentTag で必要なセグメントだけを抽出できます。
Blob全体を1つのプロトコルが使う設計と違い、複数プロトコルが同じBlob空間を0から4096まで分割して使えます。

補足

ERC8179の設計では、オンチェーンで何を保証し、何をオフチェーンに任せるかがはっきり分かれています。
コアは宣言イベントに絞り、ストレージ、重複排除、重複範囲の禁止、料金精算を持ちません。

イベントのみの設計

コアインターフェースは SSTORE を使いません。
提案では、declareBlobSegment の追加コストはおよそ3,500ガスで、状態を持つ実装のおよそ25,600ガスより大幅に安いと説明されています。

要素 ガスの目安
BLOBHASH オペコード 3
範囲検証 約6
LOG4 約1,875
イベントデータ 約512
Calldata 約1,100
合計 約3,500

状態を持つ設計では、EIP2929以降の冷たい SSTORE が22,100ガスかかります。
ERC8179は宣言をイベントに閉じることで、Blobトランザクションに追加しやすいコストに抑えています。

declareという名前

関数名は registerBlobSegment ではなく declareBlobSegment です。
register はストレージに登録して後から問い合わせられる印象を与えます。
一方、コア仕様は永続状態を作らず、イベントで「この範囲を使う」と宣言するだけです。

versionedHashを返す理由

declareBlobSegmentversionedHash を返します。
呼び出し元が後続処理で同じBlobの versionedHash を使う時、もう一度 BLOBHASH を呼ぶ必要がありません。
ルーティングコントラクトやマルチコールの中で、宣言結果をそのまま後段へ渡せます。

uint16を使う理由

Blob内のFE数は4096個で、12ビットあれば表現できます。
Solidityの標準整数型では uint16 が十分に小さく、4096を表現できます。
ABIエンコード上は uint16uint32 も32バイトへパディングされますが、uint16 は値の範囲が小さいことを型から伝えられます。
また、Queryable拡張の BlobSegment 構造体では、ストレージ上の詰め込みにも効きます。

半開区間

セグメントは [startFE, endFE) の半開区間です。
この方式では、[0, 2000)[2000, 4096) をそのまま並べるだけで、重複も隙間もないBlob全体になります。
閉区間の [startFE, endFE] では境界で +1 の計算が必要になり、境界値のミスが入りやすくなります。

Blob内ヘッダーではなくイベント

セグメント情報をBlobの先頭にヘッダーとして入れる設計も候補になります。
ERC8179はそれを採用せず、オンチェーンイベントを使います。

理由は主に4つです。

  • EVMはBlobの内容を直接読めません。
  • Blob内ヘッダーはペイロードに使えるFEを消費します。
  • 既存のBlob投稿プロトコルがBlobエンコードを変える必要があります。
  • イベントログは eth_getLogs、インデクサー、サブグラフで扱いやすい既存プリミティブです。

Blob内ヘッダーは、密に連携したBlob構築をするプロトコルには合います。
一方、汎用標準としては、既存Blobに追加コントラクト呼び出しを足すだけで済むイベント方式の方が採用しやすい設計です。

KZG検証の起点

BlobSegmentDeclared イベントは、versionedHash とFE範囲を記録します。
この値は、EIP4844のポイント評価プリコンパイルでBlob内の値を検証する時の起点になります。

イベントから検証へつなぐ流れは以下です。

イベントを起点にしたBlobセグメント検証

検証者はイベントログから versionedHashstartFEendFEcontentTag を取得します。
その後、対象BlobデータとKZG証明を取得し、FEごとにポイント評価プリコンパイルへ versionedHash、評価点 z、値 y、コミットメント、証明を渡します。
プリコンパイルは、指定したBlobコミットメントの評価点に値 y が含まれることだけを確認します。
そのバイト列がプロトコルとして正しいかどうかは、各アプリケーションが別途検証します。

オンチェーンKZG検証はFE数に比例して重くなります。
提案では1FEあたり約50,000ガス、10FEで約500,000ガス、100FEで約5,000,000ガス、Blob全体では約200,000,000ガスとされています。
そのため、オンチェーン検証は小さいセグメントに向いています。

重複範囲の扱い

ERC8179は、重複するセグメント宣言をオンチェーンで禁止しません。
重複を禁止するには、既存範囲をストレージに保存し、呼び出しごとに比較する必要があります。
これはコア仕様の低コスト設計と衝突します。

重複範囲があれば、重なったFEでは2つのプロトコルのデータが衝突します。
この問題はインデクサーが検出し、消費側のプロトコルが受け入れる宣言者や範囲を判断します。

ERC165を必須にしない理由

ERC8179は ERC165 を必須にしません。
1関数のインターフェースでは、イベントシグネチャや関数シグネチャで十分に識別できます。
また、supportsInterface はデプロイ時や問い合わせ時のコストを増やします。
実装がERC165に対応することはできますが、準拠条件には含まれません。

ERC165については以下の記事を参考にしてください。

インターフェースIDを使う実装では、以下の式で求められます。

bytes4(keccak256("declareBlobSegment(uint256,uint16,uint16,bytes32)"))

互換性

ERC8179は新しいインターフェースを追加するだけで、既存のERCやEVM仕様を変更しません。
EIP4844の BLOBHASH オペコードを前提にするため、EIP4844に対応していないチェーンでは使えません。

既にBlob全体を使っているプロトコルは、[0, 4096) を宣言するだけで採用できます。
独自のBlob登録イベントやレジストリを使っているプロトコルは、IERC_BSS を直接実装するか、独立した BlobSpaceSegments コントラクトを同じトランザクションで呼び出す方法を取れます。

テストケース

提案で示されている成功ケースは以下です。

blobIndex startFE endFE contentTag 期待結果
0 0 4096 keccak256("test.v1") Blob全体の宣言イベントを発行し、versionedHash を返す
0 2000 4096 keccak256("test.v1") 部分セグメントの宣言イベントを発行する
0 4095 4096 keccak256("test.v1") 1FEだけのセグメント宣言に成功する
0 0 2000 keccak256("test.a") 同じBlobに対する1つ目のセグメント宣言に成功する
0 2000 4096 keccak256("test.b") 同じBlobに対する2つ目のセグメント宣言に成功する

revertケースは以下です。

blobIndex startFE endFE 期待エラー
0 4096 0 InvalidSegment(4096, 0)
0 100 100 InvalidSegment(100, 100)
0 0 5000 InvalidSegment(0, 5000)
0 5000 6000 InvalidSegment(5000, 6000)
99 0 4096 NoBlobAtIndex(99)

境界として重要なのは、startFEendFE が同じ値のセグメントを許さない点です。
また、endFE = 4096 は有効ですが、4096を超える値は無効です。

参考実装

参考実装は、BLOBHASH オペコードで versionedHash を取得し、イベントを発行するだけの小さいコントラクトです。

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.24;

import {IERC_BSS} from "./IERC_BSS.sol";

contract BlobSpaceSegments is IERC_BSS {
    uint16 internal constant MAX_FIELD_ELEMENTS = 4096;

    function declareBlobSegment(
        uint256 blobIndex,
        uint16 startFE,
        uint16 endFE,
        bytes32 contentTag
    ) external returns (bytes32 versionedHash) {
        if (startFE >= endFE || endFE > MAX_FIELD_ELEMENTS) {
            revert InvalidSegment(startFE, endFE);
        }

        assembly {
            versionedHash := blobhash(blobIndex)
        }

        if (versionedHash == bytes32(0)) revert NoBlobAtIndex(blobIndex);

        emit BlobSegmentDeclared(versionedHash, msg.sender, startFE, endFE, contentTag);
    }
}

実装上の読みどころは、ストレージ変数を持たない点です。
MAX_FIELD_ELEMENTS で範囲上限を固定し、blobhash(blobIndex) の戻り値がゼロなら対象Blobなしとしてrevertします。
それ以外の情報は、BlobSegmentDeclared イベントとしてログに残します。

セキュリティ

ERC8179は、イベントで区間を宣言する軽量な標準です。
そのため、宣言の正しさ、データ内容、宣言者の正当性をすべてオンチェーンで保証する設計ではありません。

虚偽の宣言

BLOBHASH は現在のトランザクションに含まれるBlobに対してだけ非ゼロ値を返します。
そのため、別トランザクションのBlobを勝手に参照する宣言はできません。
一方、同じトランザクション内のBlobは呼び出しチェーン上のコントラクトから参照できます。
制約は呼び出し元単位ではなく、トランザクション単位です。

重複するセグメント

同じFE範囲に複数の宣言が出ることは許可されています。
オンチェーン上で排他制御をしないため、どの宣言を採用するかはインデクサーや消費側プロトコルが決めます。
重複したFEではデータが衝突するため、インデクサーは重複範囲を検出して警告できます。

スパム宣言

セグメント宣言には、Blobトランザクションの基本ガス、declareBlobSegment の実行ガス、Blobガスが必要です。
実用的なデータを持たない宣言は、宣言者自身のコストになります。
他の利用者のBlobを参照してスパム宣言することは、BLOBHASH の性質上できません。

フロントランニング

他者のBlobに対するフロントランニングは成立しにくい設計です。
攻撃者が同じ contentTag や範囲を先に宣言したいなら、自分のトランザクションに同じBlobデータを含めてBlob料金を支払う必要があります。
他者のトランザクションに含まれるBlobの versionedHash を、自分のトランザクションから BLOBHASH で取得することはできません。

contentTagのなりすまし

誰でも任意の contentTag を使ってイベントを発行できます。
contentTag は所有権を表す仕組みではありません。
そのため、プロトコルやインデクサーは、chainIdcontentTag、宣言者アドレスの組み合わせで正規の宣言者を管理する必要があります。

リオーグ

セグメント宣言は、Blobトランザクションを含むブロックのファイナリティに従います。
リオーグでブロックが取り消された場合、そのブロック内の宣言イベントも取り消されます。
インデクサーは、取り消されたブロックのイベントをロールバックする必要があります。

重複イベント

同じ引数で declareBlobSegment を複数回呼ぶと、同じ内容のイベントが複数発行されます。
実装は重複排除しません。
インデクサーは、versionedHashdeclarerstartFEendFEcontentTag に加えて、ログインデックスも含めて個別イベントとして扱います。

インデクサーの信頼モデル

重複範囲の検出、宣言の追跡、重複イベントの扱いはオフチェーンインデクサーが担います。
ただし、インデクサーを信頼する必要はありません。
誰でもイベントログを読み直し、自分のルールで同じ宣言集合を再構築できます。

KZG検証コスト

ポイント評価プリコンパイルはFEごとにコストがかかります。
小さいメッセージならオンチェーン検証できますが、Blob全体をFEごとに検証するとブロックガス制限を超えます。
検証コントラクトは、宣言された [startFE, endFE) のうち必要なFEだけを対象にする設計が必要です。

プリコンパイルが保証するのは、指定された評価点に値 y が含まれることです。
y の中身がJSON、バイナリメッセージ、証明データとして正しいかどうかは、プロトコル固有のパーサーと検証ロジックで確認します。

過大宣言

プロトコルは、実際に書いた量より大きい範囲を宣言できます。
例えば、1000FEだけを書いたのに [0, 4096) を宣言できます。
EVMはBlob内容を直接読めないため、コア仕様だけでは過大宣言を防げません。

消費側は、宣言を保証ではなく主張として扱います。
正確な帰属が必要な時は、既知のエンコード規則、KZG証明、正規宣言者の一覧を組み合わせて判断します。

Blobデータの削除

EIP4844のBlobデータは、約18日後にコンセンサスレイヤーノードから削除されます。
一方、BlobSegmentDeclared イベントは実行レイヤーのログとして残り続けます。
削除後も「この範囲を宣言した」という事実は追えますが、参照先のBlobデータは通常ノードから取得できません。
長期検証が必要なプロトコルは、Blobデータや証明を別途保存する設計が必要です。

Queryable拡張のストレージ増加

Queryable拡張は、versionedHash ごとにセグメント配列を保存します。
悪意ある利用者が同じBlobへ多数のセグメントを宣言すると、読み出しコストが膨らみます。
この拡張を使う実装では、ページサイズに上限を設け、実装ごとの制限を明記する必要があります。

最後に

今回は「EIP4844のBlobをFE単位のセグメントに分けて、複数プロトコルが同じBlob空間を共有できるようにするERC8179」についてまとめてきました。

ERC8179は、Blob共有の全体設計ではなく、どのFE範囲を誰がどの用途で使うかを宣言する最小インターフェースです。
コア仕様をイベントのみに絞ることで、安いガスで導入しやすくし、重複検出や料金精算はオフチェーンやプロトコル固有の設計へ分けています。
小さいセグメントをKZG証明で検証する用途では、BlobSegmentDeclaredversionedHash とFE範囲をつなぐ起点になります。

他でも色々記事を書いているのでぜひよろしければ読んでいってください!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?