FABRKNT
Inside Reth — シンク・拡張・SDK
Reth スタック — シンク・拡張・SDK
レッスン 11 / 17·CONTENT10 分25 XP
コース
Inside Reth — シンク・拡張・SDK
レッスンの役割
CONTENT
順序
11 / 17

レッスン8 — ノードビルダー API をステップで組み立てる

問い

ExEx は既存 Ethereum ノードを拡張。Reth SDK はコンポーネントを組み立てて自前の App-chain を Rust で構築可能。「purpose-built EVM L1」thesis がコンパイル可能なバイナリに化けるのはここ。with_typeswith_componentswith_add_onslaunch の理由は?

原理(最小モデル)

  • 素朴な「Reth 全体 fork」が破滅的 3 理由. アップストリーム分岐(毎週 update、rebase 地獄)+ 抱え込みたくない表面積(1 サブシステム変えたいのに 200K 行抱え込み)+ レビューコスト(監査 / セキュリティ / 規制が新クライアント扱い)。
  • 修正方針 = fork ではなく組み立てる. 実際に変えたいサブシステムだけ上書き、残りは Reth をライブラリ依存。
  • 6 差し替えポイント. pool / network / executor / consensus / payload / add_ons。
  • Builder パターンが「変えるものだけ書く」を実現. struct 渡しは型推論早々に崩れる + all-or-nothing 強制 → 流れるようなチェーン呼び出しに。
  • 3 軸ビルダー. types(block/tx/header レイアウト、他の全てを支える)→ components(ランタイムサブシステム)→ add_ons(RPC、ExEx)。
  • with_types 先頭の理由. 型が他全てを支える load-bearing、未確定型でコンポーネント指定すると静かに混乱 or コンパイラと戦う。
  • with_components で base.method() チェイン. EthereumNode::components() 取って .pool(...) / .network(...) で個別上書き、デフォルトは見えないまま。
  • launch の本物の仕事. MDBX 開く + P2P 起動 + sync Tokio タスク spawn + RPC サーバー配線。

具体例

最終形:

fn main() {
    Cli::parse_args()
        .run(async move |builder, _| {
            let handle = builder
                .with_types::<EthereumNode>()
                .with_components(
                    EthereumNode::components().pool(CustomPoolBuilder::default())
                )
                .with_add_ons(EthereumAddOns::default())
                .launch()
                .await?;
            handle.wait_for_node_exit().await
        })
        .unwrap();
}

素朴な fork:

git clone https://github.com/paradigmxyz/reth my-chain
cd my-chain
# 好きなように crates/ を編集
cargo build

破滅的 3 理由:

  1. アップストリーム分岐 — Paradigm 毎週リリース、fork は rebase 地獄 → 半年で安全アップグレード不可
  2. 抱え込みたくない表面積 — 1 サブシステム変えたくて fork したのに 全て 抱え込み(読んだことないコードのバグ、セキュリティパッチ、決して触らないがビルドし続けるモジュール)
  3. レビューコスト — レビュアーには変更 50 行と未変更 200K 行の区別不可 → 監査 / セキュリティ会社 / 規制当局が新クライアント扱い

6 差し替えポイント:

サブシステム何を変えるか本番例
Pool受付ルール、優先レーンTempo は payments tx を gas 価格より優先するレーン
Networkピアポリシー、プライベートサブネットバリデータが互いを優先する private gossip subnet
Executorカスタム Opcode、precompile、ガス表Hyperliquid は HyperBFT 専用 precompile 追加
ConsensusPoS → HyperBFT、PoA、TendermintHyperliquid HyperBFT、Berachain Polaris
PayloadブロックビルダーMEV-aware、アプリ固有順序(Tempo payments-first)
Add-onsカスタム JSON-RPC、ExExtidx の tidx_* namespace、検閲耐性監視 ExEx

残り(sync オーケストレータ / MDBX スキーマ / ヘッダーダウンロード / sender 復元 / ハッシングステージ)は Reth デフォルト。

最初の試案(struct 渡し)の 2 UX 問題:

struct NodeConfig<P, N, E, C, Pl> {
    pool: P,
    network: N,
    executor: E,
    consensus: C,
    payload: Pl,
}
  1. 毎回全フィールド書く羽目 — カスタマイズしていないものまで(all-or-nothing 強制)
  2. 型推論早々に崩れる — 各コンポーネントが独自ジェネリックパラメータ、単一 struct にまとめると推論効かず行き詰まった型シグネチャ

Builder パターン:

let handle = builder
    .with_pool(CustomPool::default())
    // 残りはスキップ — デフォルト
    .launch()
    .await?;

with_* が新しいビルダー型を返す、デフォルトは見えないまま。

3 軸ビルダー:

  • types: block/tx/header レイアウト、engine API
  • components: ランタイムサブシステム
  • add-ons: RPC、ExEx

with_types::<EthereumNode>() が先頭の理由: 型が他全てを支える。tx 構造を変えると pool/executor/payload/network 全てがその新 tx 型を使う必要 → SDK は 型バンドルを先に commit させる。

with_components の base.method() チェイン:

.with_components(
    EthereumNode::components()
        .pool(CustomPoolBuilder::default())   // 上書き
        // .network はデフォルト
        // .executor はデフォルト
        // .consensus はデフォルト
        // .payload はデフォルト
)

PoolBuilder トレイト(おおよそ):

trait PoolBuilder<Node>: Send {
    type Pool;
    fn build_pool(self, ctx: &BuilderContext<Node>)
        -> impl Future<Output = eyre::Result<Self::Pool>>;
}

コンポーネントは事前に構築されて渡されるのではなく、遅延ビルドされる — 前のビルダーステップが組み立てたコンテキストを必要とするから。同じ形が 6 ビルダー全部に。

with_add_ons + launch:

.with_add_ons(EthereumAddOns::default())
.launch()

Add-ons = load-bearing でない(RPC namespace、engine API 拡張、ExEx インストール)。.launch() で本物の仕事: MDBX 開く + P2P 起動 + sync Tokio タスク spawn + RPC サーバー配線 → NodeHandle 返す。

失敗例(誤解)

「fork してから時間と共に rebase で追従」— 間違い。半年で rebase 不可能化、Paradigm の毎週 update 取り込み困難 → 安全アップグレード不可。fork は何年も運用するチェーンに通用しない

with_components を先に書いてもよい」— 間違い。型が未確定でコンポーネント指定 → ① 各コンポーネントビルダーを未確定型でジェネリックにする(コンパイラと戦う)か、② .with_components 引数で型を暗黙 commit(静かに混乱)。with_types を先頭に置く

「全フィールド明示が安全」— 逆効果。デフォルトを書くと Reth のライブラリバージョンアップで型が変わるたびに全ファイル書き直し。変えるものだけ書く + デフォルトは暗黙据え置き

ステップで組み立てる

Step 1: fork の 3 破滅理由

アップストリーム分岐 / 抱え込み表面積 / レビューコスト → 何年も運用に通用しない。

Step 2: 6 差し替えポイント

pool / network / executor / consensus / payload / add_ons。

Step 3: Builder パターンの 2 UX 解決

毎回全フィールド書かない + 型推論崩れない。

Step 4: 3 軸ビルダー

types(先頭、load-bearing)→ components(ランタイム)→ add_ons(RPC、ExEx)。

Step 5: with_components の base.method() パターン

EthereumNode::components() で base、.pool(CustomBuilder) で上書き、デフォルトは暗黙。

Step 6: PoolBuilder トレイトの遅延ビルド

build_pool(self, ctx) でコンテキスト渡し、6 ビルダー全部に同じ形。

Step 7: launch の本物の仕事

MDBX 開く + P2P 起動 + sync Tokio タスク spawn + RPC サーバー配線。

答え合わせ

  • fork が「何年も運用するチェーン」に通用しない理由: ① Paradigm の毎週 update を rebase で追従不可能(半年で限界)、② 1 サブシステム変えたいのに 200K 行抱え込み、③ 監査 / セキュリティ / 規制が新クライアント扱い → コスト爆発。
  • with_types::<EthereumNode>() が先頭である理由: 型が他全てを支える load-bearing。tx 構造を変えると pool/executor/payload/network 全てがその新型を使う必要、SDK は型バンドルを 先に commit させてチェーン残りを型認識可能にする。
  • 「変えるものだけ書く + デフォルトは暗黙」が成立する仕組み: EthereumNode::components() が base、.pool(CustomBuilder) が個別上書き。Reth のライブラリバージョンアップで残り 5 のデフォルト型が変わってもユーザコードは触れない → 80% 継承 + 20% カスタム で長期メンテ可能。

合格基準

  • fork の 3 破滅理由を即答できる。
  • 6 差し替えポイント(pool / network / executor / consensus / payload / add_ons)を言える。
  • Builder パターンの 2 UX 解決を即答できる。
  • 3 軸ビルダー(types / components / add_ons)の順序と理由を言える。
  • launch の 4 起動仕事を言える。

まとめ(3行)

  • 「fork ではなく組み立てる」= 何年も運用するチェーンの保守可能な唯一の物語、6 差し替えポイント(pool / network / executor / consensus / payload / add_ons)+ 残りは Reth デフォルト。
  • 3 軸ビルダー(types 先頭 = load-bearing / components ランタイム / add_ons RPC + ExEx)+ Builder パターンで「変えるものだけ書く + デフォルト暗黙」。
  • launch が MDBX + P2P + sync Tokio + RPC を配線、Paradigm の毎週 update が 80% に流れ込み + あなたの fork 由来表面積は 20% に留まる。