レッスン8 — ノードビルダー API をステップで組み立てる
問い
ExEx は既存 Ethereum ノードを拡張。Reth SDK はコンポーネントを組み立てて自前の App-chain を Rust で構築可能。「purpose-built EVM L1」thesis がコンパイル可能なバイナリに化けるのはここ。with_types → with_components → with_add_ons → launch の理由は?
原理(最小モデル)
- 素朴な「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 理由:
- アップストリーム分岐 — Paradigm 毎週リリース、fork は rebase 地獄 → 半年で安全アップグレード不可
- 抱え込みたくない表面積 — 1 サブシステム変えたくて fork したのに 全て 抱え込み(読んだことないコードのバグ、セキュリティパッチ、決して触らないがビルドし続けるモジュール)
- レビューコスト — レビュアーには変更 50 行と未変更 200K 行の区別不可 → 監査 / セキュリティ会社 / 規制当局が新クライアント扱い
6 差し替えポイント:
| サブシステム | 何を変えるか | 本番例 |
|---|---|---|
| Pool | 受付ルール、優先レーン | Tempo は payments tx を gas 価格より優先するレーン |
| Network | ピアポリシー、プライベートサブネット | バリデータが互いを優先する private gossip subnet |
| Executor | カスタム Opcode、precompile、ガス表 | Hyperliquid は HyperBFT 専用 precompile 追加 |
| Consensus | PoS → HyperBFT、PoA、Tendermint | Hyperliquid HyperBFT、Berachain Polaris |
| Payload | ブロックビルダー | MEV-aware、アプリ固有順序(Tempo payments-first) |
| Add-ons | カスタム JSON-RPC、ExEx | tidx の 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,
}
- 毎回全フィールド書く羽目 — カスタマイズしていないものまで(all-or-nothing 強制)
- 型推論早々に崩れる — 各コンポーネントが独自ジェネリックパラメータ、単一 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% に留まる。