レッスン0 — テストゲート — この tier では全アプリがテスト green で初めて完了
問い
ここまでの 4 ティアはソースを 読む フェーズだった。ここからは 作る フェーズに入る。だが「読んだ」「作った」「たぶん動く」を、どうやって「正しい」と区別するのか? 自分で書いたコードの正しさは、誰がどうやって証明するのか?
注: この Building コースのコードブロックは「実行可能な最小例」と「概念説明の抜粋」が混在する。各レッスンの指示(抜粋/実行)に従うこと。
原理(最小モデル)
- テストスイートが green になるまで、レッスンは完了でない。 判定は green か未完了の 2 択だけ。「読んだ」「作った」「たぶん動く」は完了条件にならない。本気のインフラを動かすチーム(TigerBeetle / Cloudflare / PostgreSQL)に「読んだら正しそうだった」を答えとする組織はひとつもない。
- なぜ Building だけ厳しいか。 Foundations / Intermediate は検証済みコードを 読む フェーズだった。Building は自分でコードを 書く フェーズ。書いたコードの正しさは作者が証明するしかなく、テストがなければ「正しさ」は主張にしかならない。
- 2 つの再帰パターン。 ① pin した mainnet fork(
PINNED_BLOCKを repo 内定数に — pin しないとテストが非決定的になり CI が意味を失う)② differential testing(参照実装 = Revm 以外の provider のdebug_traceと一致を assert — 「見た目の妥当性」でなく「同じ入力で参照と一致」)。 - テストは検証手段でなく実行可能仕様。 実装後に書くと仕様が「今の実装の追認」になる。先に書けば仕様を実装から独立に定義でき、実装を仕様へ合わせる設計になる。バグはこの差分から見つかる。
具体例
全アプリが従う scaffold:
my-app/
├── Cargo.toml # workspace
├── src/
│ └── lib.rs # アプリ本体
├── tests/
│ ├── integration.rs # async / RPC / DB の境界をまたぐ
│ └── fixtures/ # golden テスト入力(tx ハッシュ・ブロック番号・期待出力)
├── foundry.toml # Solidity サーフェスがある場合
└── test/
└── *.t.sol # Solidity 側の forge テスト
パターン 1 — pin した mainnet fork:
// tests/integration.rs
use alloy::providers::ProviderBuilder;
const PINNED_BLOCK: u64 = 18_500_000;
const FORK_RPC: &str = "https://eth.merkle.io";
#[tokio::test]
async fn searcher_finds_known_opportunity() {
let provider = ProviderBuilder::new()
.connect_http(FORK_RPC.parse().unwrap());
// PINNED_BLOCK で AlloyDB-backed Revm を構築
// searcher を走らせる
// 期待される機会が見つかることを assert
// P&L が史実と一致することを assert
}
パターン 2 — differential testing:
#[tokio::test]
async fn simulator_matches_geth_debug_trace() {
for tx_hash in HISTORICAL_TX_HASHES {
let our_trace = our_simulator.trace(tx_hash).await;
let geth_trace = alchemy_provider.debug_trace_transaction(tx_hash).await;
assert_traces_equivalent(&our_trace, &geth_trace);
}
}
differential testing は、コンセンサスで定義された挙動を再実装するコードに対する gold standard。「本当に正しい?」への唯一の誠実な答えがこれだ。
失敗例(誤解)
「先にプロトタイプを作り、設計が固まってからテストを書く」は誤り — 一見合理的だが、実装先行だと仕様が実装の追認になり本番品質に届かない。Reth / Revm / Foundry の開発現場は test-first か test-alongside が前提。このティアはその現場基準をそのまま採用する。
ここまでで「green か未完了の 2 択」「テストは実行可能仕様」は着地した。ここから 10 本のアプリを作る。各レッスンはこの gate を越えさせる作りになっている。
🛑 予測。 なぜこのルールは Building (Expert) では厳しく、Foundations / Intermediate では適用されないのか?(答え: 前者は検証済みコードを 読む フェーズ、後者は自分で 書く フェーズ。書いたコードの正しさは、テストでしか「主張」を「証明」に変えられない。だから書くフェーズでだけ gate が要る。)
アプリ種別ごとの「テスト済み」最低ライン
| アプリ | 最低テスト gate |
|---|---|
| MEV searcher | Forked-state テスト — 過去の実機会を再現し P&L が正であることを assert。Reorg テスト — 1 ブロック reorg を bundle が生き残るか正しく巻き戻る |
ExEx インデクサ(tidx walk-through) | Fixture chain replay — 既知の Notification::ChainCommitted / ChainReverted を流し込んで、導出状態が golden reference と一致することを assert |
| Custom RPC エンドポイント | Integration テスト — ノードを in-process で起動、新メソッドを HTTP で叩いて JSON レスポンスを assert。エラーパス(不正パラメータ、欠損ブロック)も網羅 |
| ウォレットバックエンド | Roundtrip テスト — 署名済み tx が元に decode で戻る。Nonce invariant — 連続呼び出しが連続 nonce を返し、欠損も重複も無い |
| EIP-7702 sponsor | Replay 防止テスト — 同じ auth tuple は 2 度 sponsor できない。ガス会計テスト — sponsor が正しい額を払い、ユーザは 0 を払う |
| カスタム cheatcode | Differential テスト — Rust precompile と参照実装の Solidity が、1000 件の fuzz 入力で同じ出力を返す |
| Swap aggregator | Forked-state テスト — pin したブロックの実 Uniswap V3 pool に対して quote を取り、既知の正答との差が ε 以内 |
| Capstone(order router) | End-to-end fork テスト — order を投入し、router が分割 / ルーティング / 着地 / fill 報告するまでを観察 |
| Revm validation | Differential テスト — mainnet の小範囲の各ブロックで、Revm のトレースが Revm 以外のプロバイダの debug_traceTransaction 出力と一致 |
| Machine payments (HTTP 402) | Integration テスト — 支払い無しのリクエストは 402、有効な micropayment 付きはリソースを返す。Replay 防止テスト — 同じ payment が 2 リクエストを満たせない |
各行が最低ライン。実際の本番システムはこの上に fuzz、invariant、chaos テストを積む。純 Rust アプリ(MEV searcher・インデクサ・ウォレットバックエンド・sponsor)は Cargo.toml + src + tests だけ。Solidity サーフェスを持つアプリ(cheatcode・aggregator・capstone)は Rust と Foundry の両スイート。
各レッスン終了時に ship するもの
完了 = 公開可能な artifact が次を備えた状態:
- リポジトリ(Git でも local でも)
- アプリの説明を書いた
README - 全依存を pin した
Cargo.toml(適用可能ならfoundry.tomlも) - 実装が入った
src/ - 上の表の gate スイートが入った
tests/ - ローカルで再現可能に通る
cargo test(適用可能ならforge test) - pin した mainnet fork ブロック(または fixture chain)がテストファイルに記録されている
どれか欠ければ未完了。artifact と証明はセットで ship、もしくは ship しない。
合格基準
1 行ゲートチェック。 Building レッスンを「完了」と主張する前に答える: 「このアプリが正しいことを示すコマンドは何で、現在の終了コードは何か?」 1 文で答えられないなら、まだ作っていない。
まとめ(3行)
- このティアの完了条件は 1 つ — テストスイートが green になること。green か未完了の 2 択で、「たぶん動く」は完了でない(読むフェーズと違い、書いたコードの正しさは作者がテストで証明する)。
- 2 つの再帰パターン: pin した mainnet fork(
PINNED_BLOCK定数で決定的に)と differential testing(参照 provider のdebug_traceと一致 = 再実装コードの gold standard)。 - テストは実行可能仕様 — test-first / test-alongside で書き、実装を仕様に合わせる。次レッスンの MEV searcher から、受け入れテストを実装より先に置く。
次のレッスン(レッスン1)
最小限の MEV Searcher を Rust で作る。まず受け入れテストを書き、実装なしで fail を確認し、pass するまで実装を進める。順序は固定 — テストが先、コードが後。