レッスン7 — NodeBuilder コンセンサススロット(カスタムコンセンサスを配線)
問い
カスタム Consensus 実装がある(前モジュールで書いた)。Malachite か自前エンジンが票を駆動する。この 2 つはどうやって動くノードになるのか? 答え: NodeBuilder 上で 1 つのビルダー、1 つの実装、1 つのチェーンメソッド呼び出しだけ — カスタム mempool やカスタム EVM を差し込むのと完全に同じ形。
原理(最小モデル)
- ConsensusBuilder は 6 コンポーネントの 1 つ. pool / network / executor / consensus / payload / add_ons の中の 1 スロット。すべて同じビルダーパターン。
- 「票は Reth で検証しない」. Reth の Consensus trait は 合意後 のブロックを検証する。投票は CL(Malachite / 自作エンジン)側で行われ、Engine API で結果が EL に渡る。
- EL → CL の境界 = Engine API.
engine_newPayload(CL → EL: 検証して)/engine_forkchoiceUpdated(CL → EL: head はこれ、finalized はこれ)。 - 検証エラーが view change をトリガー.
validate_block_pre_executionがエラー → Reth がPayloadStatus::Invalidを返す → CL が別 proposer を選ぶ。検証は 決定論的 でなければならない(split-brain 防止)。
具体例
データ経路を 1 枚で:
sequenceDiagram
participant Network as P2P Network
participant CL as Tempo Consensus<br/>(Malachite)
participant Driver as Malachite Driver
participant EL as Reth<br/>(EL, TempoConsensus)
Network->>CL: バリデータ票 / 提案
CL->>Driver: Input 処理
Driver->>Driver: Vote keeper / RSM
Driver->>CL: Decide(block)
CL->>EL: engine_newPayloadV4(block)
EL->>EL: TempoConsensus::validate_block_pre_execution
EL->>EL: revm 経由で実行
EL->>EL: TempoConsensus::validate_block_post_execution
EL->>CL: PayloadStatus(VALID)
CL->>EL: engine_forkchoiceUpdatedV4(finalized)
Network->>CL: confirmation をブロードキャスト
ConsensusBuilder trait:
pub trait ConsensusBuilder<Node: FullNodeTypes>: Send {
type Consensus: FullConsensus<Node::Primitives>;
fn build_consensus(
self,
ctx: &BuilderContext<Node>,
) -> impl Future<Output = eyre::Result<Self::Consensus>> + Send;
}
カスタム Consensus impl + ビルダー + NodeBuilder 配線:
以下は概念スニペット(配線ポイント説明のための抜粋。実行時は不足型・実装を補う)。
use reth_node_builder::{NodeBuilder, NodeHandle};
use reth_chainspec::ChainSpec;
pub struct TempoConsensus {
validator_set: TempoValidatorSet,
chain_spec: Arc<ChainSpec>,
}
impl<B: Block> Consensus<B> for TempoConsensus {
type Error = ConsensusError;
fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), Self::Error> {
// Tempo 固有 pre-execution チェック:
// - proposer が validator set に含まれているか?
// - 署名は有効か?
// - round 番号は正しいか?
todo!()
}
// ... 他メソッド
}
pub struct TempoConsensusBuilder {
validator_set: TempoValidatorSet,
}
impl<Node: FullNodeTypes> ConsensusBuilder<Node> for TempoConsensusBuilder
where
Node::Primitives: NodePrimitives,
{
type Consensus = TempoConsensus;
async fn build_consensus(
self,
ctx: &BuilderContext<Node>,
) -> eyre::Result<Self::Consensus> {
Ok(TempoConsensus {
validator_set: self.validator_set,
chain_spec: ctx.chain_spec(),
})
}
}
async fn main() -> eyre::Result<()> {
let validator_set = TempoValidatorSet::load_from_chainspec(&chain_spec)?;
let consensus_builder = TempoConsensusBuilder { validator_set };
let handle = NodeBuilder::new(config)
.with_types::<TempoNode>()
.with_components(
TempoComponents::default()
.consensus(consensus_builder)
)
.launch()
.await?;
handle.wait_for_shutdown().await?;
Ok(())
}
CL 側の Engine API 呼び出し:
async fn on_decide(block: TempoBlock, engine_api: EngineApiClient) -> Result<()> {
let payload_status = engine_api
.new_payload_v4(block.to_execution_payload())
.await?;
if payload_status.status == PayloadStatus::Valid {
engine_api
.fork_choice_updated_v4(ForkchoiceState {
head_block_hash: block.hash(),
safe_block_hash: block.hash(),
finalized_block_hash: block.hash(),
}, None)
.await?;
}
Ok(())
}
失敗例(誤解)
「2f+1 quorum チェックは TempoConsensus に入る」— 間違い。Reth の Consensus trait は ブロック を検証するもので、票を検証するものではない。投票は consensus client(Malachite / CometBFT / 自作)側で起こる。Reth の Consensus trait は合意 後 のブロックを検証する。
「検証エラーが返ったら chain が止まる」— 間違い。検証エラー → Reth が PayloadStatus::Invalid → CL に「無効」と伝える → CL は view change で別 proposer を選ぶ。検証エラーが liveness 回復をトリガーする。
🛑 予測。 Reth の NodeBuilder にカスタムコンセンサスを配線する。ビルダーに渡すべき 4 つは何か? ヒント: trait 実装、validator set、署名方式、もう 1 つ。(答え: ① Consensus trait の実装、② validator set、③ 署名方式の選択 / 公開鍵、④ chain spec(fork heights、genesis、precompile アドレス)。これらが ConsensusBuilder のフィールド + BuilderContext から組み立てられる。)
ステップで組み立てる
Step 1: 6 コンポーネントスロットを言える
pool / network / executor / consensus / payload / add_ons。
Step 2: ConsensusBuilder の 1 メソッドを覚える
build_consensus(self, ctx) -> Consensus — それだけ。BuilderContext から chain spec、db、network、precompile などを取り出して Consensus 実装に渡す。
Step 3: EL ↔ CL の境界
| 場所 | 責務 |
|---|---|
| CL(Malachite) | 投票、quorum 検知、view change、リーダー選出 |
| EL(Reth + Consensus impl) | ブロック検証、EVM 実行、state 永続化 |
| Engine API | 両者を繋ぐ JSON-RPC(newPayload / forkchoiceUpdated / getPayload) |
Step 4: 検証の決定論性を保つ
Consensus 実装はホットパスで走る + 決定論的でなければならない:
- ブロックごとミリ秒単位
- アロケーションは慎重に(ヒープを荒らさない)
- Validator set 参照はキャッシュ
- バリデータごとの素朴な ECDSA ではなく BLS や閾値署名で検証
- 入力が同じなら出力が同じ(毎回同じ答え)
Step 5: スケッチ
実装スケッチ(コンパイル不要):
TempoValidatorSet— 必要フィールドは?(アドレス / 投票ウェイト / BLS 公開鍵)TempoConsensus::validate_header— 必須チェック 3 つは?- 起動シーケンス —
TempoNodeは chainspec からどう validator set をロードするか?
答え合わせ
- 2f+1 quorum 検知の場所: consensus client(Malachite / CometBFT / 自作)。Reth は 合意後 のブロックを検証する。
- 検証エラーで起きること:
PayloadStatus::Invalidを返す → CL に伝わる → CL が view change → 別 proposer。view change が liveness 回復。 - 検証決定論性が重要な理由: 非決定論的だと別バリデータと意見が分かれて split-brain 発生 = safety 違反。「キャッシュ参照 / BLS 検証 / 慎重アロケーション」全部この前提のため。
合格基準
- 6 コンポーネントスロットを即答できる。
- ConsensusBuilder の 1 メソッド名(
build_consensus)を言える。 - 「票は CL、ブロック検証は EL」を境界として言える。
- 検証エラー → view change の連鎖を辿れる。
まとめ(3行)
- ConsensusBuilder は NodeBuilder の 6 スロットの 1 つ、他コンポーネントと同形(pool / network / executor / payload / add_ons の仲間)。
- 票は CL(Malachite)で検知、ブロック検証は EL(Reth + Consensus impl)。Engine API(newPayload / forkchoiceUpdated)が両者を繋ぐ。
- 検証は 決定論的 でなければならない(同入力で同出力)— split-brain を防ぐコア前提。