レッスン8 — 本番 MEV(Mempool・ExEx・シミュレーション)
問い
ペンディング tx が mempool に届く。80 ミリ秒後、あなたのバンドルは当たったか外れたか — 同じ tx を 5ms 速くデコードし、結果を 10ms 速くシミュレーションし、ビルダーに 2ms 先に提出した競合に敗れる。Ethereum のブロックタイムは 12 秒なのに、本気の MEV パイプラインは end-to-end <100ms を狙う — なぜそこまでタイトか?
原理(最小モデル)
- 6 段パイプライン. Mempool → デコード → シミュレータ → 戦略 → バンドル組成 → 送信。
- Mempool 取り込み 2 経路. Alloy WebSocket 購読(簡単・遅い) / devp2p 直接参加(最速・最難)。
sol!マクロが型安全デコードの土台.L1StandardBridgeEvents::decode_raw_logが topic0 でバリアントマッチ → 型付きイベントでパターンマッチ。- シミュレーションは parent block ピン.
latestではなく ターゲットスロットの親ブロック に対して fork DB。シミュレーション内の被害 tx が「もう実行済み」状態にならないように。 - ExEx でウォームキャッシュ. プールリザーブ / 過去 tx インデックス / reorg 対応 state 差分を ExEx で持ち、サーチャーがネットワーク再取得を避ける。
- 送信経路 2 種. Flashbots / MEV-share(プライベート、強)/ 特定ビルダー直接(低レイテンシ)。
- 焼かれる 4 ポイント. Reorg / シミュレータ古い state / Gas griefing / Toxic flow。
具体例
パイプライン:
flowchart LR
M[Mempool<br/>ExEx + devp2p] --> D[デコード<br/>Alloy sol!]
D --> S[シミュレータ<br/>Revm + DB]
S --> St[戦略<br/>Rust ロジック]
St --> B[バンドル組成<br/>Alloy encode]
B --> Sub[送信<br/>Flashbots / 直接]
本物の ExEx デコードパターン(paradigmxyz/reth-exex-examples/op-bridge より):
use alloy_sol_types::{sol, SolEventInterface};
sol!(L1StandardBridge, "l1_standard_bridge_abi.json");
use crate::L1StandardBridge::{
ETHBridgeFinalized, ETHBridgeInitiated, L1StandardBridgeEvents,
};
fn decode_chain_into_events(
chain: &Chain,
) -> impl Iterator<Item = (...)> {
chain
.blocks_and_receipts()
.flat_map(|(block, receipts)| {
block.body().transactions_iter()
.zip(receipts.iter())
.map(move |(tx, receipt)| (block, tx, receipt))
})
.flat_map(|(block, tx, receipt)| {
receipt.logs.iter()
.filter(|log| OP_BRIDGES.contains(&log.address))
.map(move |log| (block, tx, log))
})
.filter_map(|(block, tx, log)| {
L1StandardBridgeEvents::decode_raw_log(log.topics(), &log.data.data)
.ok()
.map(|event| (block, tx, log, event))
})
}
型付きイベントでパターンマッチ:
match event {
L1StandardBridgeEvents::ETHBridgeInitiated(ETHBridgeInitiated {
amount, from, to, ..
}) => {
// デポジットを自分のDBに記録
}
L1StandardBridgeEvents::ETHBridgeFinalized(ETHBridgeFinalized {
amount, from, to, ..
}) => {
// 引き出しを自分のDBに記録
}
_ => continue,
}
シミュレーション:
use revm::Evm;
use revm::primitives::{TxKind, U256};
let mut evm = Evm::builder()
.with_db(forked_db) // ブロックNでのメインネット状態
.with_external_context(())
.build();
evm.cfg_mut().chain_id = 1;
evm.tx_mut().caller = bot_address;
evm.tx_mut().transact_to = TxKind::Call(target);
evm.tx_mut().data = tx_data;
let result = evm.transact()?;
let profit = compute_profit(&result.state);
送信経路:
| 経路 | レイテンシ | プライバシー |
|---|---|---|
| Flashbots / MEV-share | 中 | 強(mempool 公開しない) |
| 特定ビルダーへ直接 | 低 | ビルダー次第 |
失敗例(誤解)
「latest に対してシミュレーション」— 間違い。シミュレータの「latest」ビューでは被害 tx が 既に実行済み に見える → 機会を見逃す or 利益が嘘になる。ターゲットスロットの正確な親ブロック に対して fork。
「mempool 公開で十分」— 間違い。公開すると競合サーチャーが先回り。Flashbots / MEV-share で mempool に公開しない プライベート提出が標準。
「ExEx は通知だけのもの」— 間違い。ExEx を プライベート mempool / ウォームキャッシュ として使うのが本気のパターン。プールリザーブを ExEx で温める → シミュレータが再取得不要 → 数 ms 削減。
🛑 予測。 Ethereum のブロックタイムは 12 秒、なのに本気の MEV パイプラインは end-to-end <100ms を狙う。なぜそこまでタイトか? 残りの ~11.9 秒を何が食う?(答え: ① 競合サーチャー数千が同じ機会を狙う → 先着順で 1 ms 単位の競争、② ネットワーク伝播(ペンディング tx 検知から自分のマシンまでに既に数十 ms)、③ ブロックプロポーザータイミング(ビルダーは slot 終了直前まで bundle を accept、最後の数 ms が支配的)。「12 秒の予算」ではなく「数千の競争相手より速い予算」。)
ステップで組み立てる
Step 1: 6 段パイプラインを順に言える
Mempool → デコード → シミュレータ → 戦略 → バンドル組成 → 送信。各段が Rust モジュール。
Step 2: sol! + decode_raw_log パターン
sol!(Bridge, "abi.json") → BridgeEvents::decode_raw_log(topics, data) → 型付きイベント → パターンマッチ。手書き ABI パースなし。
Step 3: シミュレーション parent block ピン
AlloyDB(provider, BlockId::number(parent)) + LRU キャッシュ → 同じ読み取りをネットワーク再取得しない。
Step 4: ExEx を 3 用途で使う
① カスタム DEX トレードインデクサ、② プールリザーブのウォームキャッシュ、③ reorg 対応 state 差分フィード。サーチャーが ExEx 出力を消費。
Step 5: 焼かれる 4 ポイント
| 失敗 | 対策 |
|---|---|
| Reorg | ChainReorged 通知でバンドル後の整合 |
| シミュレータ古い state | parent block ピン |
| Gas griefing | 優先手数料カーブを実時間監視 |
| Toxic flow | 分類器で罠を弾く |
答え合わせ
- 3 段ネストの iterator を fold に潰せるか: 可能だが選ばない理由は 遅延評価と早期フィルタ。
flat_mapでストリーミング、filterがブリッジアドレスでないログを早期に削除 → デコードはマッチした少数のログだけ。fold だと全ログを accumulator に積んでから処理 = 余計な仕事。 latestシミュレーションの破綻: バンドル[自 tx_A, 被害 tx, 自 tx_B] でシミュレーション、latest` には被害 tx が 既に含まれている(mempool から確定済み)→ シミュレータの被害 tx は no-op、利益計算が嘘 → 実際に出すと利益ゼロかマイナス。- reorg で当たったバンドルの P&L: 自 ETH(バンドル先頭で支払い) → 巻き戻し、被害者 ETH → 巻き戻し、ガス → 巻き戻し(バンドル全体が含まれていたブロックが消える)= 全 P&L 巻き戻し。ただし バックランの DEX トレードがブロック含むことで価格を動かしていた場合、その状態変化を見て次のブロックで別の機会を見つけられる場合あり。ExEx の
ChainRevertedハンドラ で reorg 直後の再評価。
合格基準
- 6 段パイプラインを順に言える。
sol!デコードパターン(decode_raw_log+ パターンマッチ)を書ける。- シミュレーションを parent block ピンで設計できる。
- ExEx をプライベート mempool / ウォームキャッシュとして使う発想がある。
- 焼かれる 4 ポイントを即答できる。
まとめ(3行)
- 6 段パイプライン(Mempool → デコード → シミュレータ → 戦略 → バンドル → 送信)、end-to-end <100ms 予算、競争相手より速い予算。
sol!型安全デコード + parent block ピンシミュレーション + ExEx ウォームキャッシュが本気のパターン。- 焼かれる 4 点(Reorg / 古い state / Gas griefing / Toxic flow)を全部押さえないと利益が嘘になる。