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

レッスン8 — 最小の単一リーダー BFT を Rust で作る

問い

OP Stack のドキュメントを開く。Arbitrum のドキュメントを開く。Hyperliquid の launch 時のブログを開く。どれも「sequencer を時間をかけて分散化する」のバリエーションを言っている。翻訳すれば: launch 時には、毎ブロックを生成する 1 台のマシンがあり、特定の鍵による署名だけが唯一のコンセンサス。それだけ。これは Reth の上に Rust 約 100 行で出荷できる。 その約 100 行は何か?

原理(最小モデル)

  • 単一 sequencer = launch 時のコンセンサス. 全ブロックの proposer 役を独占 + 署名 + 緊急停止権限。Liveness は単一障害点だが view change 不要。速度 = 1 RTT。
  • 3 つの仕事. 構築(tx 順序 + timestamp)+ 署名(ECDSA で authority 証明)+ ブロードキャスト(Engine API → Reth EL + P2P)。
  • 検証は「sequencer 署名か?」の 3 行. ECDSA 復元 → expected と比較 → 違えば拒否。あとは標準 Ethereum 検証。
  • 段階的分散化が標準パターン. 単一 → 2-of-3 multisig → リーダーローテーション → 本物の BFT(Tendermint / HotStuff)。

具体例

CentralizedSequencer 実装(~100 行):

以下は概念スニペット(流れを示すための抜粋。実行時は不足型・実装を補う)。

use alloy_primitives::{Address, B256};
use alloy_signer::Signer;
use alloy_signer_local::PrivateKeySigner;
use reth_engine_primitives::ForkchoiceState;
use reth_rpc_engine_api::EngineApiClient;

pub struct CentralizedSequencer {
    signer: PrivateKeySigner,
    sequencer_address: Address,
    engine_api: EngineApiClient,
    block_period: Duration,  // e.g., 2 秒
}

impl CentralizedSequencer {
    pub async fn run(&self) -> eyre::Result<()> {
        let mut ticker = tokio::time::interval(self.block_period);
        loop {
            ticker.tick().await;
            self.produce_one_block().await?;
        }
    }

    async fn produce_one_block(&self) -> eyre::Result<()> {
        // 1. 現 head 上に payload を構築するよう Reth に依頼
        let payload_attrs = PayloadAttributes {
            timestamp: now_seconds(),
            prev_randao: B256::random(),
            suggested_fee_recipient: self.sequencer_address,
            // ...
        };

        let forkchoice_state = self.current_forkchoice().await?;
        let response = self.engine_api
            .fork_choice_updated_v4(forkchoice_state, Some(payload_attrs))
            .await?;
        let payload_id = response.payload_id.expect("must have payload id");

        // 2. Reth が payload を構築する時間を少し待つ
        tokio::time::sleep(Duration::from_millis(500)).await;

        // 3. 構築済みの payload を取得
        let payload = self.engine_api.get_payload_v4(payload_id).await?;

        // 4. Payload hash に署名する (authority の証明)
        let payload_hash = payload.execution_payload.block_hash();
        let signature = self.signer.sign_hash(&payload_hash).await?;

        // 5. 署名済 payload を Reth に投入 (peer にもブロードキャスト)
        let signed_block = SignedPayload {
            payload: payload.execution_payload,
            sequencer_signature: signature,
        };

        self.engine_api
            .new_payload_v4(signed_block.payload.clone())
            .await?;

        // 6. Finalize マーク (単一 sequencer = 即時 finality)
        let new_head = signed_block.payload.block_hash();
        let new_forkchoice = ForkchoiceState {
            head_block_hash: new_head,
            safe_block_hash: new_head,
            finalized_block_hash: new_head,
        };
        self.engine_api
            .fork_choice_updated_v4(new_forkchoice, None)
            .await?;

        // 7. peer にブロードキャスト
        self.broadcast(signed_block).await?;

        Ok(())
    }
}

検証側(カスタム Consensus 実装、3 行のみ):

impl<B: Block> Consensus<B> for CentralizedConsensus {
    fn validate_block_pre_execution(
        &self,
        block: &SealedBlock<B>,
    ) -> Result<(), ConsensusError> {
        // 唯一の「コンセンサス」チェック: sequencer の署名か?
        let signature = block.sequencer_signature()?;
        let signer = signature.recover_address(&block.hash())?;

        if signer != self.expected_sequencer {
            return Err(ConsensusError::InvalidSequencer);
        }

        // 標準的な Ethereum 系のチェック
        self.validate_basic_block(block)?;

        Ok(())
    }
}

2-of-3 multisig への進化:

pub struct MultisigSequencer {
    signers: Vec<PrivateKeySigner>,
    threshold: usize,                 // = 2
    engine_api: EngineApiClient,
}

impl MultisigSequencer {
    async fn produce_block(&self) -> eyre::Result<SignedPayload> {
        let payload = self.build_payload().await?;

        let mut signatures = Vec::new();
        for signer in &self.signers {
            if let Some(sig) = self.try_sign(signer, &payload).await {
                signatures.push(sig);
                if signatures.len() >= self.threshold {
                    break;
                }
            }
        }

        if signatures.len() < self.threshold {
            return Err(eyre!("not enough signers available"));
        }

        Ok(SignedPayload {
            payload: payload.execution_payload,
            sequencer_signatures: signatures,
        })
    }
}

L1 launch のシーケンス:

ステージコンセンサス分散化の度合いTVL safety
Day 0単一 sequencerなしチームを信頼
Month 32-of-3 multisig3 オペレータ2-of-3 セットを信頼
Month 12ローテーション proposer~10 バリデータ1 つでも生きていれば liveness
Year 2本物の BFT(Tendermint/HotStuff)30 以上のバリデータ2/3+ Byzantine 耐性

失敗例(誤解)

「これはコンセンサスではない、ただの信頼権威」— 部分的に正しい。これも コンセンサス ではある — 1 つの決定(次のブロック)への合意。意思決定者が 1 人 いるだけ。トレードオフは 信頼仮定(sequencer 1 人が正直)を liveness(view change 不要)と引き換えに。launch 時点なら許容できる仮定。

「最初から本物 BFT を入れるべき」— 間違い。Slashing ロジックのバグは壊滅的 + 検証に時間がかかる。よくあるパターン = 有効化するが上限を低く設定して launch、信頼が積み上がるにつれて上限を上げる。

🛑 予測。 Hyperliquid、Tempo、すべての OP Stack chain、Arbitrum — launch 時にすべてが走らせているコンセンサスは何か? HotStuff ではない。Tendermint でもない。(ヒント: そのどちらよりも単純。)(答え: 単一 sequencer。1 つの鍵が全ブロックに署名、view change なし、quorum なし、validator set なし。100 行の Rust で実装可能。)

ステップで組み立てる

Step 1: 3 つの仕事を言える

構築 + 署名 + ブロードキャスト。

Step 2: produce_one_block の 7 ステップ

  1. forkchoice_updated で payload 構築リクエスト
  2. 500ms 待つ
  3. get_payload で取得
  4. payload_hash に署名
  5. new_payload で Reth に投入
  6. forkchoice_updated で finalize マーク
  7. P2P ブロードキャスト

Step 3: 検証の 3 行を覚える

let signer = signature.recover_address(&block.hash())?;
if signer != self.expected_sequencer { return Err(...); }
self.validate_basic_block(block)?;

Step 4: 段階的分散化を辿れる

Day 0(単一)→ Month 3(2-of-3)→ Month 12(ローテーション + slot 適格性)→ Year 2(本物の BFT、Malachite で)。

Step 5: スケッチ

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

  1. SignedPayload struct(sequencer 署名付きカスタムブロックエンベロープ)
  2. CentralizedConsensus::validate_block_pre_execution(ECDSA 復元と比較を完備)
  3. current_forkchoice のスケッチ(現 head をローカルでどう追跡?)
  4. Mempool の置き場所(ヒント: sequencer ではない、別コンポーネント)

答え合わせ

  • 「単一リーダーコンセンサス」がコンセンサスと呼べる理由: 1 つの値(次のブロック)への合意がコンセンサスの定義。意思決定者が 1 人なら値はその意思決定者の言う通りになる。
  • 信頼仮定 → liveness の交換: launch 時には許容できる。低 TVL + ロードマップ上の項目 + 速く出荷できる、で正当化される。
  • 2-of-3 multisig が現実的な launch パターン: Optimism / Arbitrum / Base はこの形で launch、鍵はチーム + 監査人 + ノードオペレータ。HA 向上 + 1 鍵ダウンでも継続。

合格基準

  • produce_one_block の 7 ステップを順に言える。
  • カスタム Consensus の 3 行検証を書ける。
  • 段階的分散化 4 ステージを辿れる。
  • 「launch 時の信頼仮定は liveness の代償」を 1 文で説明できる。

まとめ(3行)

  • 単一 sequencer = Reth + ~100 行の Rust + 3 行の Consensus 検証で出荷できる「コンセンサス」。
  • Day 0(単一)→ Month 3(2-of-3)→ Month 12(ローテーション)→ Year 2(本物の BFT)が L1 分散化の標準シーケンス。
  • 速く出荷して段階的に分散化する — それが Hyperliquid / Optimism / Arbitrum がやった道。