【EIP4844】Blobで遊ぶ
概要
EIP4844とはblob transactionと呼ばれる新しいトランザクションタイプを導入するEthereumのアップデートです。blobは1個あたり約127KBの容量を持つデータで、calldataよりも安価に利用することができます。
詳細な背景については以下の記事をご参照ください。
EIP4844はDencunアップデートとして、今年2024年3月にメインネットリリースが予定されており、テストネットのgoerli, sepoliaでは既に導入されています。この記事ではsepoliaを使って一足先にEIP4844で遊んでみます。
なお、現在solidityやethersなどの主要なライブラリはEIP4844に未対応ですが、将来的に対応するようになれば、本記事の内容は古くなると予想されます。記事公開から時間を置いて読まれる際はご注意ください。
blob transactionを投げる
blob txを投げるためのCLIツールであるblob-utils
を使います。
README
の通りに、go build
した後./blob-utils command
で利用することができます。
blob txは使い捨てのアドレスで送ることをお勧めします。筆者が試した限りでは、blob tx のキャンセルはできません。そのため、ガス不足などでブロックに取り込まれなかった場合は、後続のtxが打てなくなってしまい、GOXします。以下のサイトなどでランダムなアドレスと秘密鍵を生成し、使う分だけsepolia ethをデポジットしてください。
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-price
とmax-fee-per-blob-gas
を高めに設定することがポイントです。デフォルト値を使うと取り込まれません。
出力されたトランザクションをblock exploreで見てみます。
トランザクションタイプが3
(blob transaction)であることが確認できます。
blobの詳細はblobscanで見ることができます。
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 price
が19.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.ts
のconfig
の部分を以下のように編集します。
const config: HardhatUserConfig = {
solidity: "0.8.23",
networks: {
sepolia: {
url: "https://1rpc.io/sepolia",
accounts: ["PRIVATE_KEY"]
}
}
};
BLOBHASH
を実行するコントラクトを作りたいのですが、あいにくsolidityはまだ対応していません。そのため、バイトコードを直接実装する必要があります。幸い、先人が既に実装してくれています。
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を取得します。メソッド名を変えていない場合、calldata
は0x4ebf82e50000000000000000000000000000000000000000000000000000000000000000
となっているはずです。
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_KEY
とBLOB_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の中の
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 = 1
は48656c6c6f2c20626c6f620a0000000000000000000000000000000000000000
("Hello, blob"のUTF-8表示)が入っています。今回はメッセージが短いので、
./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の仕様についてはこちらのドキュメントが詳しいです。
上記の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が成功したことを確認すれば完了です!
-
正確にはblobを表す多項式
y = f(x)
が特定の点(a, b)
を通ることを検証することができるprecompileです。詳細は上記の解説記事をご参照ください。 ↩︎
Discussion