EIP-3668について解説します。
EIP-3668とは
EIP-3668はオフチェーンの状態データをコントラクトによって検証可能にする提案です。
現在ブロックチェーンで状態を扱おうとするといくつかの問題があります。
- 大きなデータは扱えない
- 扱えるサイズでもコストが非常に高い
- とはいえオフチェーンの情報は信用ができない
- データの書き込みにはトランザクションが必要でガス代がかかってコストがかかる
これらを解決するのが当EIPです。解決策として、
- オフチェーンを使います
- オフチェーンの情報をオンチェーンで(コントラクトで)検証するようにします
- これはCCIP(Cross Chain Interoperability Protocol)と言います。
- CCIPは異なるチェーンの情報をやり取りするための仕組みです
現在Final Statusになっています。
実装例
以下の3stepを実装します。
- コントラクトに問い合わせる
- コントラクトからもらった情報をオフチェーンに問い合わせる
- オフチェーンからもらった情報をコントラクトで検証する
シーケンス図は以下のようになります。
┌──────┐ ┌────────┐ ┌─────────────┐
│Client│ │Contract│ │Gateway @ url│
└──┬───┘ └───┬────┘ └──────┬──────┘
│ │ │
│ somefunc(...) │ │
├─────────────────────────────────────────────────►│ │
│ │ │
│ revert OffchainData(sender, urls, callData, │ │
│ callbackFunction, extraData) │ │
│◄─────────────────────────────────────────────────┤ │
│ │ │
│ HTTP request (sender, callData) │ │
├──────────────────────────────────────────────────┼────────────►│
│ │ │
│ Response (result) │ │
│◄─────────────────────────────────────────────────┼─────────────┤
│ │ │
│ callbackFunction(result, extraData) │ │
├─────────────────────────────────────────────────►│ │
│ │ │
│ answer │ │
│◄─────────────────────────────────────────────────┤ │
│ │ │
例としてエアドロップされたERC20のコントラクトを例に挙げます。引数に渡したアドレスの残高がいくらであるかを返す関数 balanceOf を以下のように実装します。
function balanceOf(address addr) public override view returns(uint balance) {
if(claimed[addr]){
return super.balanceOf(addr);
} else {
revert OffchainLookup(
address(this),
urls,
abi.encodeWithSelector(Gateway.getSignedBalance.selector, addr),
Token.balanceOfWithSig.selector,
abi.encode(addr)
);
}
}
この例ですとclaimedに存在するアドレス(トークンを請求済みのアドレス)の場合は標準的なコントラクトの情報のみを返すようになっています。それ以外の場合はrevertでOffchainLookupというエラーを返しています。トークンを請求していないアドレスの残高はコントラクトに載ってない実装になっています。
OffchainLookupは以下のような構造になっています。
error OffchainLookup(
address sender,
string[] urls,
bytes callData,
bytes4 callbackFunction,
bytes extraData
)
オフチェーンで何を参照すべきなのかがここに示されています。具体的な例では
OffchainLookup(
"0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
["http://localhost:8080/"],
"0xce93871500000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8",
"0x5f52f060",
"0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8"
)
誰が送信しました、どのurlを見てください、送信すべきデータ、検証用のcallback関数、そのほか色々
みたいなものを返しています。
clientはこのデータを受け取った後、指定されたurlに以下のようなリクエストを送信します。
requestBody: '{
"sender":"0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9",
"data":"0xce93871500000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8"
}'
このリクエストを処理するプログラムは送信者のアドレスの残高を署名付きで返します。
HTTP/2 200
content-type: application/json; charset=UTF-8
...
{"data": "0x3635c9adc5dea00000"} => 1000000000000000000000
ゼロがたくさんでこのトークンをたくさん持っているようですね!価値がゼロであればいくら持っていても価値ゼロですが...
この情報を全面的に信用していいのかというとそんなことはありません。不正な情報かもしれませんし意図しない値を返すように改竄されているかもしれません。
そこでclientはこの値が正しいのかどうかコントラクトに問い合わせをします。
OffchainLookupにあったcallbackFunctionを署名付きの値を引数にして呼び出します。今回の場合、balanceOfWithSigを呼び出します。
function balanceOfWithSig(bytes calldata result, bytes calldata extraData) external view returns(uint) {
(address addr) = abi.decode(extraData, (address));
uint balance = super.balanceOf(addr);
return balance + _getBalance(addr, result);
}
ここでresultに署名情報を渡して _getBalance関数の中で検証しています。
function _getBalance(address addr, bytes memory result) public view returns(uint) {
(uint256 balance, bytes memory sig) = abi.decode(result, (uint256, bytes));
if(claimed[addr]) {
return 0;
} else {
address recovered = keccak256(
abi.encodePacked(
"\x19Ethereum Signed Message:\n32",
keccak256(abi.encodePacked(balance, addr))
)).recover(sig);
require(_signer == recovered, "Signer is not the signer of the token");
return balance;
}
}
これにより、balanceOfWithSigはコントラクトに存在する残高と署名が検証できたオフチェーンの残高を足し合わせて残高として返すことができました。どのような署名にするか何を検証すべきかはこのEIPで定義されていません。処理内容によって各々考える必要があるでしょう。
今回説明をシンプルにするためにbalanceOfを例として挙げましたが、トランザクションを発生させなければならないtransferやapproveなどにもOffchainLookupを実装することは可能です。またbalanceOfではシンプルな数値しか返しませんが、大きなバイナリやJSONデータをオンチェーンで検証することも可能です。何よりも重要なことはこの一連の流れではトランザクションが発生していません。つまりガスコストが一切かかっていません。
利点と考慮すべき点
このEIP-3668の良いところは
- 無限のオフチェーンストレージを利用できる
- ガス代を節約しトランザクションの発行を抑制できる
- 実行結果は検証可能性がある
考慮しなければならないデメリットは以下の点でしょうか。
- 検証方法は定義されていないので処理内容によって都度用意して実装する必要がある
- クライアントはコントラクトが悪意のある動作をしないことを確認する必要がある(現状と一緒ですね)
- オンチェーン -> オフチェーン -> オンチェーンと通信するのでオフチェーンの部分で意図しない情報が混入する可能性がある(例えばIPアドレスとか)
このEIP-3668を実装するとコントラクトもクライアントも実装は複雑になります。それでもトランザクションコストを下げたり大きなデータを扱うことができるのは一定の価値を感じられる方もいるのではないでしょうか。有名なところだとENSやLens Protocolで採用実績があります。
今回はあくまでもオフチェーンをブロックチェーン以外のサーバーとして紹介しましたがオフチェーンは他のブロックチェーンでも良いはずです。安価なブロックチェーンを利用しつつ良きタイミングで本命のブロックチェーンに書き込む。この提案はEIP-5559 Cross Chain Write Deferral Protocolとして提案されています。まだDraft Statusです。
結論
この記事の結論としてこのEIP-3668には以下のようなことがいえます。
- オフチェーンによるコスト削減と同時にコントラクトのセキュリティを持たせることができる
- オンチェーンへの書き込みを無期限に遅延、もしくは無視することができる
- マルチチェーンサービスに活用することができる
- 設計は複雑になる可能性がある