FABRKNT
Consensus Engineering — Reth で L1 のコンセンサスを作る
Reth でコンセンサスを作る
レッスン 9 / 12·CONTENT16 分45 XP
コース
Consensus Engineering — Reth で L1 のコンセンサスを作る
レッスンの役割
CONTENT
順序
9 / 12

レッスン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: スケッチ

実装スケッチ(コンパイル不要):

  1. TempoValidatorSet — 必要フィールドは?(アドレス / 投票ウェイト / BLS 公開鍵)
  2. TempoConsensus::validate_header — 必須チェック 3 つは?
  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 を防ぐコア前提。