FABRKNT
Building with the Stack — 実アプリを作る
アプリケーションパターン
レッスン 1 / 11·CONTENT18 分35 XP
コース
Building with the Stack — 実アプリを作る
レッスンの役割
CONTENT
順序
1 / 11

レッスン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 searcherForked-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 sponsorReplay 防止テスト — 同じ auth tuple は 2 度 sponsor できない。ガス会計テスト — sponsor が正しい額を払い、ユーザは 0 を払う
カスタム cheatcodeDifferential テスト — Rust precompile と参照実装の Solidity が、1000 件の fuzz 入力で同じ出力を返す
Swap aggregatorForked-state テスト — pin したブロックの実 Uniswap V3 pool に対して quote を取り、既知の正答との差が ε 以内
Capstone(order router)End-to-end fork テスト — order を投入し、router が分割 / ルーティング / 着地 / fill 報告するまでを観察
Revm validationDifferential テスト — 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 が次を備えた状態:

  1. リポジトリ(Git でも local でも)
  2. アプリの説明を書いた README
  3. 全依存を pin した Cargo.toml(適用可能なら foundry.toml も)
  4. 実装が入った src/
  5. 上の表の gate スイートが入った tests/
  6. ローカルで再現可能に通る cargo test(適用可能なら forge test
  7. 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 するまで実装を進める。順序は固定 — テストが先、コードが後