🔥

【EIP4844】Blobで遊ぶ

2024/02/01に公開

概要

EIP4844とはblob transactionと呼ばれる新しいトランザクションタイプを導入するEthereumのアップデートです。blobは1個あたり約127KBの容量を持つデータで、calldataよりも安価に利用することができます。

詳細な背景については以下の記事をご参照ください。

https://zenn.dev/qope/articles/b8d09ae260f1aa

EIP4844はDencunアップデートとして、今年2024年3月にメインネットリリースが予定されており、テストネットのgoerli, sepoliaでは既に導入されています。この記事ではsepoliaを使って一足先にEIP4844で遊んでみます。

なお、現在solidityやethersなどの主要なライブラリはEIP4844に未対応ですが、将来的に対応するようになれば、本記事の内容は古くなると予想されます。記事公開から時間を置いて読まれる際はご注意ください。

blob transactionを投げる

blob txを投げるためのCLIツールであるblob-utilsを使います。

https://github.com/Inphi/blob-utils

READMEの通りに、go buildした後./blob-utils commandで利用することができます。

blob txは使い捨てのアドレスで送ることをお勧めします。筆者が試した限りでは、blob tx のキャンセルはできません。そのため、ガス不足などでブロックに取り込まれなかった場合は、後続のtxが打てなくなってしまい、GOXします。以下のサイトなどでランダムなアドレスと秘密鍵を生成し、使う分だけsepolia ethをデポジットしてください。

https://privatekeys.pw/keys/ethereum/random

blobとして送信する、適当なテキストファイルを作成しておきます。

echo Hello, blob > message.txt

次にblob txを送信します。

./blob-utils tx --rpc-url https://1rpc.io/sepolia --blob-file message.txt --private-key PRIVATE_KEY --to 0x00 --gas-price 100000000000 --chain-id 11155111 --max-fee-per-blob-gas 100000000000

PRIVATE_KEYは先程作成したランダムなアドレスの秘密鍵を入れます。gas-pricemax-fee-per-blob-gasを高めに設定することがポイントです。デフォルト値を使うと取り込まれません。

出力されたトランザクションをblock exploreで見てみます。

https://sepolia.etherscan.io/

トランザクションタイプが3 (blob transaction)であることが確認できます。

blobの詳細はblobscanで見ることができます。

https://sepolia.blobscan.com/

https://sepolia.blobscan.com/tx/TX_HASHでtxの詳細を見ることができます。

blobsの中からblobのハッシュをクリックすると、blobの中身を見ることができます。右側のview asの項目をUTF-8にすると先程のテキストファイルの内容を確認できます。

成功です!

blobでどれほどgasを節約できるか

1つのblobあたりgasは2^17 = 131072と定められています。これにblob gas priceを掛けたものがblobのgas feeになります。先程のtxではblob gas price19.9gweiでしたので、131072*19.9gwei = 0.0026ETHがblobを使った場合のgas feeとなります。

先程のmessage.txtの中身は12bytesでした。calldataは(non-zeroの)1bytesあたり16gasなので、合計で12*16=192 gasになります。そのときのgas priceは2.0 gweiだったので、192*2.0 gwei = 0.0000000384 ETHがblobを使わない場合のgas feeです。結局、少量のデータ送信ではblobを使うとむしろ高くなるということが分かりました。

ただし、もしblobの127KBを目一杯使った場合は、blobを使う場合は変わらず0.0026ETHで, blobを使わない場合は127*1024*16*2.0 gwei = 0.0041ETHですので、blobを使った方が60%程度安くなるということになります。

blob gas priceとEVMのgas priceは独立に決まるので(これはmultidimensional fee marketと呼ばれています)、タイミングによってblobとcalldataの価格比は変わります。
またmainnetリリース前ということもあり、testnetのblob需要が高まっていると考えられるので、blobの価格が上振れしてしまっている可能性もあります。

contractからblob hashを取得する

EIP4844でEVMには主に以下の二つの機能が追加されました。

BLOBHASH

BLOBHASHはtxのblob (versioned)hashを返すオペコードです。これによって、blob hashをcontractから参照することが可能になります。

point evaluation precompile

point evaluation precompileはblob hashで指定されたblobの中身の一部分が、特定の値であることを検証することができるprecompileです[1]

BLOBHASHとpoint evaluation precompileを組み合わせることで、例えば、blobの投稿時にblob hashをBLOBHASHで取得してストレージに保存しておき、後からpoint evaluation precompileで中身を証明するといったことが可能になります。

以下では、BLOBHASHオペコードをcontractから呼び出し、中身をpoint evaluation precompileで検証します。

contractのdeploy

hardhatを利用してcontractをdeployします。
hardhatのプロジェクトを初期化します。

mkdir blob-contract
cd blob-contract
npx hardhat init

コンソールでTypeScript projectを選択し、他はデフォルト設定で初期化します。hardhat.config.tsconfigの部分を以下のように編集します。

const config: HardhatUserConfig = {
 solidity: "0.8.23",
  networks: {
    sepolia: {
      url: "https://1rpc.io/sepolia",
      accounts: ["PRIVATE_KEY"]
    }
  }
};

BLOBHASHを実行するコントラクトを作りたいのですが、あいにくsolidityはまだ対応していません。そのため、バイトコードを直接実装する必要があります。幸い、先人が既に実装してくれています。

https://github.com/ethstorage/eip4844-blob-hash-getter

0x6000354960005260206000F3がそのバイトコードになります。それぞれ次のような意味になっています。

0x6000
0x35    # calldataload msg[0:32]
0x49    # datahash
0x6000
0x52    # mstore offset=0, value=datahash
0x6020
0x6000
0xF3    # return offset=0, length=32

このコントラクトを呼び出すコントラクトを下のように実装し、BlobContract.solとしてcontracts配下に置きます。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

contract ContractFactory {
    constructor(bytes memory code) payable {
        uint256 size = code.length;
        assembly {
            return(add(code, 0x020), size)
        }
    }
}

contract BlobContract {
    address public getter;
    event BlobHash(bytes32 hash);
    event ReturnData(bytes returndata);

    constructor() {
        bytes memory code = hex"6000354960005260206000F3";
        getter = address(new ContractFactory(code));
    }

    function getBlobHash(uint256 idx) public returns (bytes32) {
        bool success;
        bytes32 blobHash;
        address getter_ = getter;
        assembly {
            mstore(0x0, idx)
            success := staticcall(gas(), getter_, 0x0, 0x20, 0x0, 0x20)
            blobHash := mload(0x0)
        }
        emit BlobHash(blobHash);
        require(success, "failed to get");
        return blobHash;
    }

    function proveBlob(bytes memory input) public {
        address pointEvaluationPrecompile = address(0x0A);
        (bool success, bytes memory returndata) = pointEvaluationPrecompile
            .call{gas: 50000}(input);
        require(success, "failed to prove");
        emit ReturnData(returndata);
    }
}

このcontractのデプロイスクリプトは下記のようになります。

import { ethers } from "hardhat";

async function main() {
  const BlobContract = await ethers.getContractFactory("BlobContract");
  const blobContract = await BlobContract.deploy();
  console.log("BlobContract deployed to:", blobContract.address);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

これをscripts配下にdepoly.tsとして保存し、下のコマンドで実行します。

npx hardhat run scripts/deploy.ts --network sepolia

早速getBlobHashを呼び出してみましょう。これはblob txのidx番目のblob hashを返し、イベントを発火する関数です。blob txを送信する必要があるので、hardhatではなく、blob-utilsを使ってtxを打つ必要があります。getBlobHashを呼び出すcalldataを下記のスクリプトで生成します。

import { ethers } from "hardhat";

async function main() {
  const blobContract = await ethers.getContractAt("BlobContract", "BLOB_CONTRACT_ADDRESS")
  const calldata = blobContract.interface.encodeFunctionData("getBlobHash", [0]);
  console.log("calldata:", calldata);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

これを実行し、calldataを取得します。メソッド名を変えていない場合、calldata0x4ebf82e50000000000000000000000000000000000000000000000000000000000000000となっているはずです。

blob-utilsでtxを打ってみます。

./blob-utils tx --rpc-url https://1rpc.io/sepolia --blob-file message.txt --private-key PRIVATE_KEY --to BLOB_CONTRACT_ADDRESS --calldata 0x4ebf82e50000000000000000000000000000000000000000000000000000000000000000 --gas-price 100000000000 --chain-id 11155111 --max-fee-per-blob-gas 100000000000 --gas-limit 30000

PRIVATE_KEYBLOB_CONTRACT_ADDRESSは実際のものに変更して実行してください。

sepolia scanのtx logsの欄を見ると、BlobHashイベントが発火していることを確認することができます。

中身の01c21a7259643ea9b189afba954c23497e8ed37f7a55aa36de716fa465d863aaがblob hashとなっています。

blob scanのblob hashと一致しています!

次に、point evaluation precompileを試してみましょう。point evaluationのproofは、blob-utilsで作成することができます。

./blob-utils proof --blob-file message.txt --blob-index 0 --input-point 0000000000000000000000000000000000000000000000000000000000000001 

次のような結果が返ってくるはずです。

versionedHash 01c21a7259643ea9b189afba954c23497e8ed37f7a55aa36de716fa465d863aa 
x 0000000000000000000000000000000000000000000000000000000000000001 
y 48656c6c6f2c20626c6f620a0000000000000000000000000000000000000000 
commitment b5452ba5f92bce9cbe9a63a754e1c5333a13df63a276dd5afdc5d962bd75a3ebe9fcdbe080df84c8347bca6a7a8618ce 
proof a77d7d66625c2cbac957f95195f5b3066446e4a8e9662b59d4451d7e120779506861a97d3c215fec6f05095b54bb9367 
pointEvalInput 01c21a7259643ea9b189afba954c23497e8ed37f7a55aa36de716fa465d863aa000000000000000000000000000000000000000000000000000000000000000148656c6c6f2c20626c6f620a0000000000000000000000000000000000000000b5452ba5f92bce9cbe9a63a754e1c5333a13df63a276dd5afdc5d962bd75a3ebe9fcdbe080df84c8347bca6a7a8618cea77d7d66625c2cbac957f95195f5b3066446e4a8e9662b59d4451d7e120779506861a97d3c215fec6f05095b54bb9367

これはblobを表す多項式y=f(x)が、点(x, y) = (1, 48656c6c6f2c20626c6f620a0000000000000000000000000000000000000000)を通ることを示すproofになっています。

詳細

blobの中のi番目の要素y_iは、y_i = f(w^i)として多項式にエンコードされています。ここでwはBLS12-381のscalar field上で位数4096の乗法群を作る生成元で、 BLS12-381のscalar fieldの生成元をa = 7、BLS12-381のscalar fieldのモジュラスをpとしてw = a^{(p-1)/4096}として計算できます。
wはsagemathでは次のように計算できます。

sage: p = 52435875175126190479447740508185965837690552500527637822603658699938581184513
sage: F = FiniteField(p)
sage: a = F.multiplicative_generator()
sage: w = a**((p-1)/4096)
sage: Integer(w).hex().zfill(64)
'564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d36306'

x = 1i = 0を意味するため、blobの最初の要素には48656c6c6f2c20626c6f620a0000000000000000000000000000000000000000 ("Hello, blob"のUTF-8表示)が入っています。今回はメッセージが短いので、1 \le i < 4095ではy_iは全て0になっています。実際、x = w^1においてproofを作ってみるとy_1=0であることが確認できます。

./blob-utils proof --blob-file message.txt --blob-index 0 --input-point 564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d36306
2024/02/01 12:42:12 
versionedHash 01c21a7259643ea9b189afba954c23497e8ed37f7a55aa36de716fa465d863aa 
x 564c0a11a0f704f4fc3e8acfe0f8245f0ad1347b378fbf96e206da11a5d36306 
y 0000000000000000000000000000000000000000000000000000000000000000

その他、KZG proofの仕様についてはこちらのドキュメントが詳しいです。
https://github.com/ethereum/consensus-specs/blob/ae3ef6f330846cae283e7748f423ce54128ef6d4/specs/deneb/polynomial-commitments.md

上記のx, y, commitment, proofを単に連結したものがpointEvalInputとなっています。これを用いてpoint evaluation precompileを呼び出してみましょう。今度はblob txではないので、hardhatから呼び出すことができます。

import { ethers } from "hardhat";

async function main() {
  const blobContract = await ethers.getContractAt("BlobContract", "BLOB_CONTRACT_ADDRESS")
  const tx = await blobContract.proveBlob("0x01c21a7259643ea9b189afba954c23497e8ed37f7a55aa36de716fa465d863aa000000000000000000000000000000000000000000000000000000000000000148656c6c6f2c20626c6f620a0000000000000000000000000000000000000000b5452ba5f92bce9cbe9a63a754e1c5333a13df63a276dd5afdc5d962bd75a3ebe9fcdbe080df84c8347bca6a7a8618cea77d7d66625c2cbac957f95195f5b3066446e4a8e9662b59d4451d7e120779506861a97d3c215fec6f05095b54bb9367")
  console.log("tx hash:", tx.hash);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

sepolia scanでtxが成功したことを確認すれば完了です!

脚注
  1. 正確にはblobを表す多項式y = f(x)が特定の点(a, b)を通ることを検証することができるprecompileです。詳細は上記の解説記事をご参照ください。 ↩︎

Discussion