🤖

SUAVE&MEVM入門

2023/12/12に公開
2

この記事はEthereumアドベントカレンダー2023 (https://qiita.com/advent-calendar/2023/ethereum) 12日目の投稿です。

TL;DR

  • SUAVEの概要の説明
  • MEVMの概要と既存のEVMとの違いを確認する
  • 実際にMEVMを利用したサンプルコードの解説
  • デプロイとスクリプトの実行

前提

  • ここではMEVの基本的な説明については省きます。MEVについて知りたい人はこちらを参考にするといいです。
  • この記事ではMEVMを利用してコードを書くことがメインなのでSUAVEに関しては、SUAVEが出てきた背景とSUAVEの概要のみ説明します。

SUAVE

SUAVEが生まれた背景

現在のEthreumのブロック構築の約90%はMEV-Boostというオープンソースミドルウェアを使用しています。MEV-boostによってブロック構築を行うBlock Builderとブロックを提案するBlock Proposerに分かれるPBSをout of protocolで実現しました。これはバリデータがサードパーティーのBuilderにブロックの構築の委託をし、誰もがBuilderになれる環境を提供しました。そしてBuilder間でオークションをすることによって、より価値の高いブロックの構築を可能にしました。MEV-Boostについての詳しい情報は公式のドキュメントをご参照ください。
https://docs.flashbots.net/flashbots-mev-boost/introduction

しかし、その一方で排他的オーダーフローやBuilderを運営している会社がSearcherも同時に運用して独自のサプライチェーンを構築することでBuilderの中央集権化が問題になります。Builderが中央集権化することでEthereumの検閲耐性などに大きな影響を与える可能性があります。Builderが中央集権化に関してはこちらの記事をご参照ください。
https://mirror.xyz/0xa9BFf1967E1F76373F67C48F42f1178520bb38C6/m15QRhrE_6Nn-Ecj6wRWmHTNcxyPKtel_8whvRbztTo

そこでFlashbotsは分散型のブロック構築を実現するためのプロトコルであるSUAVE (the Single Unified Auction for Value Expression)を発表しました。

SUAVEとは

SUAVEはブロックチェーンではありますが他の汎用的なチェーンとは異なり、既存のチェーンからmempoolとBuilderの部分を切り離して、それに代替する高度に特化したplug-and-play mempoolと分散型のBlock Builderをもつネットワークを提供します。

https://writings.flashbots.net/the-future-of-mev-is-suave

SUAVEのアーキテクチャには大きく分けて3つのコンポーネントがあります。

  1. Universal Preference Environment
    Preferenceの表現と決済に特化しており、参加するすべてのチェーンのユーザーとSearcherのPreferenceを1つの場所に集約します (分かりやすくいうとShared mempoolと表現できる)。
    Preferenceとは「私はXという資産を持っており、Yが欲しく、Zで支払いたい」などのユーザーの特定の目標を表現するために署名されたメッセージのことを指します。

  2. Optimal Execution Market
    SUAVEのmempoolを、ユーザーのPreferenceに最適な執行を提供するために競争するExecutorと呼ばれる特別な当事者のネットワークを指します。
    このマーケットははOrder flowの経済的価値を認識し、ユーザー、ウォレット、その他のOrder flowを発生したアクターがその取引で最大の利益を得ることができる分散化された場所を目指しています。

  3. Decentralized Block Building
    Builderのための分散型ネットワークで、ユーザーからの暗号化されたPreferenceにアクセスし、それらをPartial blockあるいはFull blockとして構築します。

    https://writings.flashbots.net/the-future-of-mev-is-suave

SUAVE Roadmap


https://github.com/flashbots/suave-specs
SUAVEは現在最初の段階であるRigilがローンチされた状態で、まだまだ初期段階です。今後はTEE (SGX)を利用した秘密計算も導入される予定です。今ならSUAVEのSuper Early Adapterだ🫵🫵🫵

MEVM

MEVMとは

MEVユースケースのための新しいプリコンパイルを備えたEVMの強力な改良版です。MEVサプライチェーンのプリミティブをプリコンパイルとして提供しており、SUAVE上でさまざまなブロック構築(PROFなど)やOrder flow auctionの構築に役立ちます。

これは上記でも述べたBuilderのやそれ以外のアクターであるRelayなどの中央集権化しているインフラをブロックチェーンのスマートコントラクトでプログラミングできるようになるためMEVサプライチェーンの分散化に資すると考えられます。そしてSUAVE (MEVM)を使用して作られたアプリケーションのことをSUAPPと呼びます。

既存のEVMとの違い

Confidential Data Store

Confidential Data Storeは、SUAPP の機密データ(L1のトランザクション、EIP-712の署名、userOps、秘密鍵など)を保存します。
そしてConfidential Data Storeには主に4つの機能があります。

  1. Confidential Data Input
    ユーザーはConfidential Compute Requests (CCR)[1]を通じてSUAPPにConfidential Dataを送信します。
  1. Confidential Data Transfer
    Kettle[2]がCCRに成功すると、その結果は専用のトランスポートプロトコルを使って他のKettleにbroadcastされます。
  1. Data Modeling
    Confidential Data Storeはkey-valueのstorageを採用しており、さまざまな型に対応している。

    • トランザクション、EIP-712の署名、userOps
    • Bundle
    • Partial blockあるいはFull block
  2. Access Control
    プライバシーを実現するため、Peekersと呼ばれる適切な権限を持つ契約のみがデータの取得と保存を許可されます。

    https://suave.flashbots.net/technical/specs/rigil/confidential-data-store

Precompile

PrecompileはConfidential Data Storeを扱うためのものや、solidityでは高価な計算になってしまうものが実装されています。
ここではいくつか代表的なものを紹介します。
ConfidentialInputs
CCRの際に提供されたInputsを受け取る関数。

function confidentialInputs() internal view returns (bytes memory);

ConfidentialStore
Confidential dataを保存する。

function confidentialStore(BidId bidId, string memory key, bytes memory data1) internal view;

ConfidentialRetrieve
Confidential storeからデータを取得する。

function confidentialRetrieve(BidId bidId, string memory key) internal view returns (bytes memory);

NewBid
Confidential Store内のBid[3]を初期化する。

function newBid(uint64 decryptionCondition, address[] memory allowedPeekers, address[] memory allowedStores, string memory bidType);

FetchBids
指定された復号化条件に関連するすべてのBidを検索する。

function fetchBids(uint64 cond, string memory namespace) internal view returns (Bid[] memory);

Examples

Order flow auction

1つ目の例としてMEV-Shareに基づいたOrder flow auction SUAPPを紹介します。
レポジトリはこちら: https://github.com/flashbots/suapp-examples/tree/main/examples/app-ofa-private

Sample code
// SPDX-License-Identifier: UNLICENSED
 pragma solidity ^0.8.8;
 
 import "../../suave-geth/suave/sol/libraries/Suave.sol";
 
 contract OFAPrivate {
     address[] public addressList = [0xC8df3686b4Afb2BB53e60EAe97EF043FE03Fb829];
 
     // Struct to hold hint-related information for an order.
     struct HintOrder {
         Suave.BidId id;
         bytes hint;
     }
 
     event HintEvent (
         Suave.BidId id,
         bytes hint
     );
 
     // Internal function to save order details and generate a hint.
     function saveOrder() internal view returns (HintOrder memory) {
         // Retrieve the bundle data from the confidential inputs
         bytes memory bundleData = Suave.confidentialInputs();
 
         // Simulate the bundle and extract its score.
         uint64 egp = Suave.simulateBundle(bundleData);
 
         // Extract a hint about this bundle that is going to be leaked
         // to external applications.
         bytes memory hint = Suave.extractHint(bundleData);
 
         // Store the bundle and the simulation results in the confidential datastore.
         Suave.Bid memory bid = Suave.newBid(10, addressList, addressList, "");
         Suave.confidentialStore(bid.id, "mevshare:v0:ethBundles", bundleData);
         Suave.confidentialStore(bid.id, "mevshare:v0:ethBundleSimResults", abi.encode(egp));
 
         HintOrder memory hintOrder;
         hintOrder.id = bid.id;
         hintOrder.hint = hint;
 
         return hintOrder;
     }
 
     function emitHint(HintOrder memory order) public payable {
         emit HintEvent(order.id, order.hint);
     }
 
     // Function to create a new user order
     function newOrder() external payable returns (bytes memory) {
         HintOrder memory hintOrder = saveOrder();
         return abi.encodeWithSelector(this.emitHint.selector, hintOrder);
     }
 
     // Function to match and backrun another bid.
     function newMatch(Suave.BidId shareBidId) external payable returns (bytes memory) {
         HintOrder memory hintOrder = saveOrder();
 
         // Merge the bids and store them in the confidential datastore.
         // The 'fillMevShareBundle' precompile will use this information to send the bundles.
         Suave.BidId[] memory bids = new Suave.BidId[](2);
         bids[0] = shareBidId;
         bids[1] = hintOrder.id;
         Suave.confidentialStore(hintOrder.id, "mevshare:v0:mergedBids", abi.encode(bids));
         
         return abi.encodeWithSelector(this.emitHint.selector, hintOrder);
     }
 
     function emitMatchBidAndHintCallback() external payable {
     }
 
     function emitMatchBidAndHint(string memory builderUrl, Suave.BidId bidId) external payable returns (bytes memory) {
         bytes memory bundleData = Suave.fillMevShareBundle(bidId);
         Suave.submitBundleJsonRPC(builderUrl, "mev_sendBundle", bundleData);
         
         return abi.encodeWithSelector(this.emitMatchBidAndHintCallback.selector);
     }
 }

このサンプルの大まかな流れは以下になります。

  1. 最初のトランザクションを送信する (newOrder())
  2. backrunのトランザクションを送信する (newMatch())
  3. 最初のトランザクションとbackrunのトランザクション (Bid) をマージする (newMatch())
  4. マージしたトランザクションをBuilderに送信する (emitMatchBidAndHint())

この流れからわかるようにこのサンプルではユーザー / SearcherからBuilderにBundleを渡すまでをプログラムしているので、このコードではブロックの構築を行なっていません。

そしてこのコードの中で特にPrecompileが使われている部分について解説します。

saveOrder()

bytes memory bundleData = Suave.confidentialInputs();
これは先ほどのPrecompileでも紹介されたようにユーザーから送られてきたConfidential dataであるbundleDataを取得しています。このbundleDataはユーザーのトランザクションが含まれています。実際に送っているスクリプトの部分はこちら

uint64 egp = Suave.simulateBundle(bundleData);
このPrecompileはbundleが含まれるブロックを構築して、そのシミュレーションを実行します。出力は、ブロックのガス価格を返却します。

bytes memory hint = Suave.extractHint(bundleData);
このPrecompileはbundleを解釈し、"To"アドレスやcalldataなどのヒント[4]を抽出します。

Suave.Bid memory bid = Suave.newBid(10, addressList, addressList, "");
このPrecompileでは上記で述べたようにBidを初期化しています。

Suave.confidentialStore(bid.id, "mevshare:v0:ethBundles", bundleData);
このPrecompileでは上記で述べたように、Confidential dataを保存します。今回はbundleDataを保存しています。
bid.idは1つ上のnewBid()で初期化したBidの識別子になります。

emitMatchBidAndHint()

bytes memory bundleData = Suave.fillMevShareBundle(bidId);
このPrecompileではユーザーのトランザクションとバックランを結合し、エンコードされた MEV-Share Bundleを返却します。ここのbidIdnewMatch()でマージされたBidのIdを指します。

Suave.submitBundleJsonRPC(builderUrl, "mev_sendBundle", bundleData);
最後にこのPrecompileではSuave.fillMevShareBundle()で作成したBundleをRelayerに送信しています。

PEPC-REP

これは11月に行われたETH Global Istanbulで提出したMEVMを活用したプロジェクトです。
概要はこちら: https://twitter.com/keccak254/status/1726770626973536645
レポジトリはこちら: https://github.com/adachi-440/pepc-rep

Sample code
// SPDX-License-Identifier: UNLICENSED
 pragma solidity ^0.8.9;
 
 import {Suave} from "../../suave-geth/suave/sol/libraries/Suave.sol";
 
 // Uncomment this line to use console.log
 import "forge-std/console.sol";
 
 contract Pepc {
     string public boostRelayUrl = "https://relay-goerli.flashbots.net";
 
     struct Bundle {
         Suave.BidId bidId;
         bytes data;
     }
 
     struct BuiltBlock {
         Suave.BidId bidId;
         bytes data;
     }
 
     mapping(address => address) public preferences; // proposer => tokenAddress
     mapping(Suave.BidId => bool) public isRegistered; // bidId => bool
 
     event RegisterPreference(address proposer, address token);
     event SendBundleTx(Suave.BidId bidId, bytes bundleData);
     event BuildBlock(Suave.BidId bidId, bytes builderBid);
     event BidEvent(Suave.BidId bidId, uint64 decryptionCondition, address[] allowedPeekers);
 
     error InvalidAddress();
 
     /**
      * @dev Register the token address that the proposer wants to receive as a reward.
      * @param _proposer The address of the proposer.
      * @param _token The address of the token that the proposer wants to receive as a reward.
      * @notice The proposer can register only one token address.
      */
     function registerPreference(address _proposer, address _token) external {
         if (_proposer == address(0) || _token == address(0)) {
             revert InvalidAddress();
         }
 
         preferences[_proposer] = _token;
 
         emit RegisterPreference(_proposer, _token);
     }
 
     /**
      * @dev Get the token address that the proposer wants to receive as a reward.
      * @param _proposer The address of the proposer.
      * @notice If the proposer has not registered the token address, the zero address will be returned.
      */
     function getPreference(address _proposer) external view returns (address) {
         return preferences[_proposer];
     }
 
     function emitSendBundleTx(Bundle memory bundle) public payable {
         emit SendBundleTx(bundle.bidId, bundle.data);
     }
 
     /**
      * @dev Send the bundle transaction to the confidential datastore.
      * @param decryptionCondition The decryption condition of the bundle.
      * @param bidAllowedPeekers The addresses of the peekers that are allowed to peek the bundle.
      * @param bidAllowedStores The addresses of the stores that are allowed to store the bundle.
      * @notice The proposer can register only one token address.
      */
     function sendBundleTx(
       uint64 decryptionCondition,
       address[] memory bidAllowedPeekers,
       address[] memory bidAllowedStores,
       uint256 volume
       ) external payable returns (bytes memory) {
         // Retrieve the bundle data from the confidential inputs
         bytes memory bundleData = Suave.confidentialInputs();
 
         // Store the bundle and the simulation results in the confidential datastore.
 		    Suave.Bid memory bid = Suave.newBid(decryptionCondition, bidAllowedPeekers, bidAllowedStores, "pepc:v0:uncheckedBundles");
         Suave.confidentialStore(bid.id, "pepc:v0:ethBundles", bundleData);
         Suave.confidentialStore(bid.id, "pepc:v0:volumes", abi.encodePacked(volume));
         isRegistered[bid.id] = false;
 
         Bundle memory bundle;
         bundle.bidId = bid.id;
         bundle.data = bundleData;
 
         return abi.encodeWithSelector(this.emitSendBundleTx.selector, bundle);
     }
 
     /**
      * @dev Select the TOB from the bundle transactions.
      * @param blockHeight The block height to select the TOB.
      * @param blockArgs The arguments to build the TOB.
      * @notice The proposer can register only one token address.
      */
     function buildTOB(uint64 blockHeight, Suave.BuildBlockArgs memory blockArgs) external payable returns (bytes memory) {
         Suave.Bid[] memory allUncheckedBids = Suave.fetchBids(blockHeight, "pepc:v0:uncheckedBundles");
 
         Suave.Bid[] memory allBids = new Suave.Bid[](allUncheckedBids.length);
 
         // Bubble sort the bids by volume
         uint n = allUncheckedBids.length;
         for (uint i = 0; i < n; i++) {
           for (uint j = i + 1; j < n; j++) {
             Suave.Bid memory bid = allUncheckedBids[i];
             Suave.Bid memory nextBid = allUncheckedBids[j];
 
             if(isRegistered[bid.id]) continue;
 
             uint256 volume = abi.decode(Suave.confidentialRetrieve(bid.id, "pepc:v0:volumes"), (uint256));
             uint256 nextVolume = abi.decode(Suave.confidentialRetrieve(nextBid.id, "pepc:v0:volumes"), (uint256));
 
             if (volume < nextVolume) {
               allBids[i] = nextBid;
               allBids[j] = bid;
             }
             isRegistered[bid.id] = true;
           }
         }
 
         Suave.BidId[] memory allBidIds = new Suave.BidId[](allBids.length);
         for (uint i = 0; i < allBids.length; i++) {
           allBidIds[i] = allBids[i].id;
         }
         console.log("allBidIds.length");
         console.log(allBidIds.length);
 
         return buildAndEmit(blockArgs, blockHeight, allBidIds, "pepc:v0");
     }
 
     function emitBuildBlock(BuiltBlock memory builtBlock) public payable {
         emit BuildBlock(builtBlock.bidId, builtBlock.data);
     }
 
     function buildAndEmit(Suave.BuildBlockArgs memory blockArgs, uint64 blockHeight, Suave.BidId[] memory bids, string memory namespace) public virtual returns (bytes memory) {
 
 		(Suave.Bid memory blockBid, bytes memory builderBid) = this.doBuild(blockArgs, blockHeight, bids, namespace);
 		Suave.submitEthBlockBidToRelay(boostRelayUrl, builderBid);
 
 		BuiltBlock memory builtBlock;
     builtBlock.bidId = blockBid.id;
     builtBlock.data = builderBid;
 
 		return abi.encodeWithSelector(this.emitSendBundleTx.selector, builtBlock);
 	}
 
     function emitBid(Suave.Bid calldata bid) public payable {
 		  emit BidEvent(bid.id, bid.decryptionCondition, bid.allowedPeekers);
 	}
 
   function doBuild(Suave.BuildBlockArgs memory blockArgs, uint64 blockHeight, Suave.BidId[] memory bids, string memory namespace) public view returns (Suave.Bid memory, bytes memory) {
 		address[] memory allowedPeekers = new address[](2);
 		allowedPeekers[0] = address(this);
 		allowedPeekers[1] = Suave.BUILD_ETH_BLOCK;
 
 		Suave.Bid memory blockBid = Suave.newBid(blockHeight, allowedPeekers, allowedPeekers, "default:v0:mergedBids");
 		Suave.confidentialStore(blockBid.id, "default:v0:mergedBids", abi.encode(bids));
 
 		(bytes memory builderBid, bytes memory payload) = Suave.buildEthBlock(blockArgs, blockBid.id, namespace);
 		Suave.confidentialStore(blockBid.id, "pepc:v0:builderPayload", payload); // only through this.unlock
 
 		return (blockBid, builderBid);
 	}
 }

このプロジェクトの大まかな流れは以下になります (* MEVMが関係している部分に限定しています)。

  1. 複数のSearcherがswap(今回はUniswap V4を利用)のトランザクションを含んだBundleを送信
  2. 取引のボリュームの順番でBundleをソートする
  3. ソートしたBundleをもとにブロックを構築する
  4. Relayにブロックを送信する

そしてこのコードの中で特にPrecompileが使われている部分について解説します。

doBuild()

(bytes memory builderBid, bytes memory payload) = Suave.buildEthBlock(blockArgs, blockBid.id, namespace);
このPrecompileは指定されたBidIds に基づいてEthereumのブロックを構築します。構築はBidIds の順番に従って行われます。今回のケースだと取引のボリュームの順番に並び替えており、その順番でブロックが構築されます。

buildAndEmit()

Suave.submitEthBlockBidToRelay(boostRelayUrl, builderBid);
与えられた builderBid を MEV-boost Relayに提出します。今回はdoBuild()で構築されたブロックをRelayに提出することになります。

今回紹介した例以外にもサンプルコードが掲載されているので、是非チェックしてみてください。
https://github.com/flashbots/suapp-examples

How to run

ここでは実際にSUAVEを動かしてみたいと思います。Localで動かす方法とテストネットを利用する方法の2種類があります。

Local

ここではDockerでの動かし方を紹介します。

  1. レポジトリのクローン
    git clone https://github.com/flashbots/suave-geth.git

  2. SUAVEの起動
    cd suave-geth/
    make devnet-up

  3. テストのトランザクションの実行
    go run suave/devenv/cmd/main.go
    このような結果になれば成功です。

Testnet

テストネットの場合はhardhatやfoundryで以下のネットワークを追加することで動かすことができます。
ChainId: 16813125
RPC: https://rpc.rigil.suave.flashbots.net
explorer: https://explorer.rigil.suave.flashbots.net/
faucet: https://faucet.rigil.suave.flashbots.net/

How to deploy and execute contract

コントラクトのデプロイや実行を行うにはGo言語のSDKを使用することをお勧めします。
https://suave.flashbots.net/how-to/interact-with-suave/golang-sdk
ドキュメントにはTypescriptも紹介されていますが動きませんでした、、、

実際のデプロイやトランザクションを送信するコードは以下のようになります。

Sample code
 package main
 
 import (
 	"context"
 	"crypto/ecdsa"
 	"encoding/json"
 	"fmt"
 	"math/big"
 	"os"
 
 	_ "embed"
 
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/core/types"
 	"github.com/ethereum/go-ethereum/crypto"
 	"github.com/ethereum/go-ethereum/rpc"
 	"github.com/ethereum/go-ethereum/suave/e2e"
 	"github.com/ethereum/go-ethereum/suave/sdk"
 )
 
 var (
 	exNodeEthAddr = common.HexToAddress("b5feafbdd752ad52afb7e1bd2e40432a485bbb7f")
 	exNodeNetAddr = "http://localhost:8545"
 
 	// This account is funded in both devnev networks
 	// address: 0xBE69d72ca5f88aCba033a063dF5DBe43a4148De0
 	fundedAccount = newPrivKeyFromHex("91ab9a7e53c220e6210460b65a7a3bb2ca181412a8a7b43ff336b3df1737ce12")
 )
 
 var (
 	bundleBidContract = e2e.BundleBidContract
 	mevShareArtifact  = e2e.MevShareBidContract
 )
 
 func main() {
 	rpc, _ := rpc.Dial(exNodeNetAddr)
 	mevmClt := sdk.NewClient(rpc, fundedAccount.priv, exNodeEthAddr)
 
 	var mevShareContract *sdk.Contract
 	_ = mevShareContract
 
 	var (
 		testAddr1 *privKey
 		testAddr2 *privKey
 	)
 
 	var (
 		ethTxn1       *types.Transaction
 		ethTxnBackrun *types.Transaction
 	)
 
 	fundBalance := big.NewInt(100000000)
 	var bidId [16]byte
 
 	steps := []step{
 		{
 			name: "Create and fund test accounts",
 			action: func() error {
 				testAddr1 = generatePrivKey()
 				testAddr2 = generatePrivKey()
 
 				if err := fundAccount(mevmClt, testAddr1.Address(), fundBalance); err != nil {
 					return err
 				}
 				fmt.Printf("- Funded test account: %s (%s)\n", testAddr1.Address().Hex(), fundBalance.String())
 
 				// craft mev transactions
 
 				// we use the sdk.Client for the Sign function though we only
 				// want to sign simple ethereum transactions and not compute requests
 				cltAcct1 := sdk.NewClient(rpc, testAddr1.priv, common.Address{})
 				cltAcct2 := sdk.NewClient(rpc, testAddr2.priv, common.Address{})
 
 				targeAddr := testAddr1.Address()
 
 				ethTxn1, _ = cltAcct1.SignTxn(&types.LegacyTx{
 					To:       &targeAddr,
 					Value:    big.NewInt(1000),
 					Gas:      21000,
 					GasPrice: big.NewInt(13),
 				})
 
 				ethTxnBackrun, _ = cltAcct2.SignTxn(&types.LegacyTx{
 					To:       &targeAddr,
 					Value:    big.NewInt(1000),
 					Gas:      21420,
 					GasPrice: big.NewInt(13),
 				})
 				return nil
 			},
 		},
 		{
 			name: "Deploy mev-share contract",
 			action: func() error {
 				// Deploy contract
 				txnResult, err := sdk.DeployContract(mevShareArtifact.Code, mevmClt)
 				if err != nil {
 					return err
 				}
 				receipt, err := txnResult.Wait()
 				if err != nil {
 					return err
 				}
 				if receipt.Status == 0 {
 					return fmt.Errorf("failed to deploy contract")
 				}
 
 				fmt.Printf("- Mev share contract deployed: %s\n", receipt.ContractAddress)
 				mevShareContract = sdk.GetContract(receipt.ContractAddress, mevShareArtifact.Abi, mevmClt)
 				return nil
 			},
 		},
 		{
 			name: "Send bid",
 			action: func() error {
 				refundPercent := 10
 				bundle := &types.SBundle{
 					Txs:             types.Transactions{ethTxn1},
 					RevertingHashes: []common.Hash{},
 					RefundPercent:   &refundPercent,
 				}
 				bundleBytes, _ := json.Marshal(bundle)
 
 				// new bid inputs
 				targetBlock := uint64(1)
 				allowedPeekers := []common.Address{mevShareContract.Address()}
 
 				confidentialDataBytes, _ := bundleBidContract.Abi.Methods["fetchBidConfidentialBundleData"].Outputs.Pack(bundleBytes)
 
 				// Send transaction
 				txnResult, err := mevShareContract.SendTransaction("newBid", []interface{}{targetBlock + 1, allowedPeekers, []common.Address{}}, confidentialDataBytes)
 				if err != nil {
 					return err
 				}
 				receipt, err := txnResult.Wait()
 				if err != nil {
 					return err
 				}
 				if receipt.Status == 0 {
 					return fmt.Errorf("failed to send bid")
 				}
 
 				bidEvent := &BidEvent{}
 				if err := bidEvent.Unpack(receipt.Logs[0]); err != nil {
 					return err
 				}
 				hintEvent := &HintEvent{}
 				if err := hintEvent.Unpack(receipt.Logs[1]); err != nil {
 					return err
 				}
 				bidId = bidEvent.BidId
 
 				fmt.Printf("- Bid sent at txn: %s\n", receipt.TxHash.Hex())
 				fmt.Printf("- Bid id: %x\n", bidEvent.BidId)
 
 				return nil
 			},
 		},
 		{
 			name: "Send backrun",
 			action: func() error {
 				backRunBundle := &types.SBundle{
 					Txs:             types.Transactions{ethTxnBackrun},
 					RevertingHashes: []common.Hash{},
 				}
 				backRunBundleBytes, _ := json.Marshal(backRunBundle)
 
 				confidentialDataMatchBytes, _ := bundleBidContract.Abi.Methods["fetchBidConfidentialBundleData"].Outputs.Pack(backRunBundleBytes)
 
 				// backrun inputs
 				targetBlock := uint64(1)
 				allowedPeekers := []common.Address{mevShareContract.Address()}
 
 				txnResult, err := mevShareContract.SendTransaction("newMatch", []interface{}{targetBlock + 1, allowedPeekers, []common.Address{}, bidId}, confidentialDataMatchBytes)
 				if err != nil {
 					return err
 				}
 				receipt, err := txnResult.Wait()
 				if err != nil {
 					return err
 				}
 				if receipt.Status == 0 {
 					return fmt.Errorf("failed to send bid")
 				}
 
 				bidEvent := &BidEvent{}
 				if err := bidEvent.Unpack(receipt.Logs[0]); err != nil {
 					return err
 				}
 
 				fmt.Printf("- Backrun sent at txn: %s\n", receipt.TxHash.Hex())
 				fmt.Printf("- Backrun bid id: %x\n", bidEvent.BidId)
 
 				return nil
 			},
 		},
 	}
 
 	for indx, step := range steps {
 		fmt.Printf("Step %d: %s\n", indx, step.name)
 		if err := step.action(); err != nil {
 			fmt.Printf("Error: %v\n", err)
 			os.Exit(1)
 		}
 	}
 }
 
 func fundAccount(clt *sdk.Client, to common.Address, value *big.Int) error {
 	txn := &types.LegacyTx{
 		Value: value,
 		To:    &to,
 	}
 	result, err := clt.SendTransaction(txn)
 	if err != nil {
 		return err
 	}
 	_, err = result.Wait()
 	if err != nil {
 		return err
 	}
 	// check balance
 	balance, err := clt.RPC().BalanceAt(context.Background(), to, nil)
 	if err != nil {
 		return err
 	}
 	if balance.Cmp(value) != 0 {
 		return fmt.Errorf("failed to fund account")
 	}
 	return nil
 }
 
 type step struct {
 	name   string
 	action func() error
 }
 
 type privKey struct {
 	priv *ecdsa.PrivateKey
 }
 
 func (p *privKey) Address() common.Address {
 	return crypto.PubkeyToAddress(p.priv.PublicKey)
 }
 
 func (p *privKey) MarshalPrivKey() []byte {
 	return crypto.FromECDSA(p.priv)
 }
 
 func newPrivKeyFromHex(hex string) *privKey {
 	key, err := crypto.HexToECDSA(hex)
 	if err != nil {
 		panic(fmt.Sprintf("failed to parse private key: %v", err))
 	}
 	return &privKey{priv: key}
 }
 
 func generatePrivKey() *privKey {
 	key, err := crypto.GenerateKey()
 	if err != nil {
 		panic(fmt.Sprintf("failed to generate private key: %v", err))
 	}
 	return &privKey{priv: key}
 }
 
 type HintEvent struct {
 	BidId [16]byte
 	Hint  []byte
 }
 
 func (h *HintEvent) Unpack(log *types.Log) error {
 	unpacked, err := mevShareArtifact.Abi.Events["HintEvent"].Inputs.Unpack(log.Data)
 	if err != nil {
 		return err
 	}
 	h.BidId = unpacked[0].([16]byte)
 	h.Hint = unpacked[1].([]byte)
 	return nil
 }
 
 type BidEvent struct {
 	BidId               [16]byte
 	DecryptionCondition uint64
 	AllowedPeekers      []common.Address
 }
 
 func (b *BidEvent) Unpack(log *types.Log) error {
 	unpacked, err := bundleBidContract.Abi.Events["BidEvent"].Inputs.Unpack(log.Data)
 	if err != nil {
 		return err
 	}
 	b.BidId = unpacked[0].([16]byte)
 	b.DecryptionCondition = unpacked[1].(uint64)
 	b.AllowedPeekers = unpacked[2].([]common.Address)
 	return nil
 }

また、より簡単に扱うにはこのファイルを利用することをおすすめします。
https://github.com/flashbots/suapp-examples/blob/main/framework/framework.go
上記のファイルを活用すればより簡単にコントラクトのデプロイやトランザクションの送信をすることが可能です。

以下ではLocalとTestnetで異なる点のみを共有しておきます。

Local

Localの際にはノードのRPCをhttp://localhost:8545にしておく必要があります。
実際に書かれているのはこちら

Testnet

Testnetの際にはノードのRPCをhttps://rpc.rigil.suave.flashbots.netにしておく必要があります。
また、Private keyとノードのアドレスを別で設定する必要があります。

 var (
     // Target the Rigil RPC
 	exNodeNetAddr = "https://rpc.rigil.suave.flashbots.net"
     // Insert a private key you own with some SUAVE ETH
 	fundedAccount = newPrivKeyFromHex("<your_priv_key>")
 	// The public address of a Kettle on Rigil
 	exNodeEthAddr = common.HexToAddress("03493869959c866713c33669ca118e774a30a0e5")
 )

SUAVEとInterop

ここまで、MEVMの基本的な取り扱いについて説明しましたが、ここでは個人的に関心のあるInteroperabilityと絡めてお話ししたいと思います。
例えば、SUAVEをShared mempoolとして扱う (いわゆるIntents、Preferences)場合、ユーザーはSUAVEに署名を送信するため、ユーザーとしては1つのネットワークに送信していることになり、チェーンを意識する必要がなくなります。これを最近ではチェーンの抽象化 (Chain Abstraction)と呼びます。特にLayer2間においてはSUAVEがブロックの構築も行うことでShared mempoolだけでなく、Shared sequencerの役割を果たすことができるのでは?と思う方もいるでしょう (これは最初にSUAVEの説明としてplug-and-play mempoolと分散型のBlock Builderという2つの役割とリンクしています)。
https://www.maven11.com/publication/the-shared-sequencer
SUAVEがEspressoなどのShared sequencerに取って代わる存在になるのでしょうか?答えはNoであり、SUAVEと既存のShared sequencerにはそれぞれProConがあります。そして2つを組み合わせることでより強力なInteroperabilityを実現することができます。

SUAVE

Pro

  • 対象のブロックが受け入れられた場合にどのようなstateになるのかを知っている

Con

  • ブロックのatomic (inclusion)を保証できない
  • あくまでSUAVEはブロックを構築するまでなので、それが受け入れられる保証はない (Proposerではないことを示す)

Shared sequencer

Pro

  • ブロックのatomic inclusion (not execution)自体は保証している

Con

  • Shared sequencerはトランザクションの内容を知ることができない
  • 全てのトランザクションが成功している場合は実行もatomicになるが、シンプルな出入金のトランザクションではないsmart contractベースのトランザクションが含まれている中でatomic excutionを実現するのはほぼ不可能 (= atomic inclusionのメリットはほとんどない)

SUAVEとShared sequencerの組み合わせ

SUAVEがCross-rollup用のブロックを構築し、Shared sequencerでそのブロックのatomic inclusionを保証できればatomic executionの完全性が高まると考えられます。
SUAVEは2つのrollupのブロックがatomic inclusionされて、同時に実行された場合どのようなstateになるかを保証し、Shared sequencerは2つのrollupのブロックが両方とも含まれることを保証します。これによってL2でのPBSが実現できるのではないかと考えられます。


https://www.archetype.fund/media/the-little-transaction-that-could-sequencers-mev-intents-and-more

この考え方はまだ形になっていないので、もし興味がある人はぜひ議論しましょう!!

最後に

現在、Titania ResearchというMEVサプライチェーンの集権化や検閲などの問題に対する解決策を提案・開発することで、ブロック構築の側面からEthereumに貢献することを目的とした組織にコントリビューターとして活動しています。
誰もがオープンソースからcontributionを開始できるための、学習機会提供、メカニズムデザインを主とする研究や開発などを行なっております。興味がある場合はご連絡ください。
https://scandalous-stick-9ab.notion.site/Titania-Research-587cd20f07b14d259fa7d5c8d9646fc9?pvs=4

また、Titania ResearchでのMEVに関するコントリビュートとは別で、FutabaというCross-chainのインフラを開発しています。こちらにドキュメントを掲載しておくので、興味がある人はご連絡ください。
https://futaba.gitbook.io/docs/introduction/futaba-introduction

参考

MEV

https://writings.flashbots.net/the-future-of-mev-is-suave
https://writings.flashbots.net/mevm-suave-centauri-and-beyond
https://mirror.xyz/0xa9BFf1967E1F76373F67C48F42f1178520bb38C6/m15QRhrE_6Nn-Ecj6wRWmHTNcxyPKtel_8whvRbztTo
https://collective.flashbots.net/t/mev-share-programmably-private-orderflow-to-share-mev-with-users/1264
https://collective.flashbots.net/t/order-flow-auctions-and-centralisation-ii-order-flow-auctions/284

Shared sequenceer

https://www.archetype.fund/media/the-little-transaction-that-could-sequencers-mev-intents-and-more
https://dba.mirror.xyz/NTg5FSq1o_YiL_KJrKBOsOkyeiNUPobvZUrLBGceagg
https://joncharbonneau.substack.com/p/rollups-arent-real
https://prestwich.substack.com/p/the-definitive-guide-to-sequencing?sd=pf

For developers

https://suave.flashbots.net/
https://github.com/flashbots/suave-geth
https://github.com/flashbots/suapp-examples

脚注
  1. CCRはConfidential Dataを含めたSUAVEへのユーザーリクエストを指します。 ↩︎

  2. KettleはCCRを受け付け、処理し、SUAVEチェーンを維持するSUAVEネットワークのメインのアクターです。 ↩︎

  3. ここでいう"Bid"は入札を意味するのではなく、単なるデータの識別子を意味します。開発初期の名残りになっているそうです(名前変えてほしい、、、)。 ↩︎

  4. ヒントはマッチメーカーがユーザーのトランザクションとBundleをマッチングするために、Searcherが提供する情報のことを指します。 ↩︎

Discussion

kimkimkimkim

素敵な記事をありがとうございます!リファレンス含め、記事を深堀したいと思います。
下記気づいた点です:

  • newOrder() <- ここはsaveOrder()ですかね?
  • SUAVE のProCon
    • 後続の記述「SUAVEは2つのrollupのブロックがatomic inclusionされて、同時に実行された場合どのようなstateになるかを保証」を読むと、ここのProConは逆に読めるのですが、いかがですか?
Tomoki AdachiTomoki Adachi

コメントありがとうございます!!

newOrder() <- ここはsaveOrder()ですかね?
そうですね!修正しておきました!

SUAVE のProCon
完全に逆で書いてますね、、、修正しておきました!