FABRKNT
Step 1. Consensus — cargo init から single-validator devnet 構築
Contract types
レッスン 4 / 16·CONTENT30 分60 XP
コース
Step 1. Consensus — cargo init から single-validator devnet 構築
レッスンの役割
CONTENT
順序
4 / 16

レッスン3 — ConsensusBridge trait

問い

BFT consensus と EVM execution の間の契約を、いくつのメソッドで表すべきか? なぜ ちょうど 4 つ で、3 でも 5 でもないのか?

原理(最小モデル)

  • メソッドは 4 つ: build_payload / payload_ready / validate_payload / commit 数は BFT round 構造(propose → vote → decide)で決まる。build_payloadpayload_ready を 1 つにすると「投票中の先行ビルド」ができない。5 つ目(例 notify_view_change)を足すと consensus 内部事情を EL API に漏らし層分離が崩れる。
  • #[async_trait] + : Send + Sync async_traitasync fn を boxed future へ desugar し dyn 互換にする。Send + Sync super-trait bound で Arc<dyn ConsensusBridge> を actor 間で共有しても安全だと compiler が検証できる。
  • error は 3 variant: Rejected / Syncing / Internal consensus の 3 応答(反対 vote / 待つ / 停止)に対応。1 つの String error にすると consensus が文字列 parse して分岐する羽目になる。

具体例

build_payload(前 round 投票中に裏で開始)と payload_ready(自分が proposer の hot path で fetch)を分けることが、最も重要な latency trick だ。同期的に 1 メソッドでブロックを返すと、proposer は build を待ってから broadcast する。分けると build が裏で走り、hot path は「準備済みブロックを fetch」(microsecond)に縮む。sub-second block time はこれに依存する。

失敗例(誤解)

「メソッドは少ない方が良いから build と payload_ready を 1 つに」は誤り(先行ビルド不能)。「状態通知も足そうと notify_view_change を追加」も誤り(consensus 内部を EL に leak)。数は API デザインの好みでなく BFT round 構造が決める。


ここまでで「なぜ 4 メソッド・なぜ Send+Sync・なぜ 3 error」は着地した。ここから bridge.rs を組み立てる。コードは完全形。

🛑 予測。 4 メソッドのうち 3 つは CL → EL、1 つは EL → CL。どれが EL → CL 方向か? ヒント:その戻り値を consensus 側がどう待っているか。

ステップで組み立てる

Step 1: crates/consensus/Cargo.toml に依存を追加

[dependencies]
openhl-types = { workspace = true }
async-trait  = { workspace = true }
thiserror    = { workspace = true }
eyre         = { workspace = true }
  • openhl-types — trait signature がレッスン2 の 5 type を参照する。
  • async-trait — trait 内 async fnPin<Box<dyn Future>> へ desugar(native async-fn-in-trait はまだ Send bound / dyn 互換に粗さがある)。
  • thiserrorDisplay/Error を手書きせず error enum を derive。
  • eyre — catch-all な Internal 用。任意 error をバックトレース付きでラップ。

Step 2: crates/consensus/src/bridge.rs(新規ファイル全体)

//! The CL/EL contract: four messages between consensus and execution.

use async_trait::async_trait;
use openhl_types::{BlockHash, ExecutedBlock, PayloadAttrs, PayloadId, PayloadStatus};
use thiserror::Error;

/// The four-message contract between BFT consensus and EVM execution.
///
/// Every interaction between `openhl-consensus` and `openhl-evm` flows through one of these methods. Anything else is a contract leak.
#[async_trait]
pub trait ConsensusBridge: Send + Sync {
    /// CL → EL: build a candidate block on `parent`. Returns immediately; await the block via [`Self::payload_ready`].
    async fn build_payload(
        &self,
        parent: BlockHash,
        attrs: PayloadAttrs,
    ) -> Result<PayloadId, BridgeError>;

    /// EL → CL: wait for an in-flight build to complete.
    async fn payload_ready(&self, id: PayloadId) -> Result<ExecutedBlock, BridgeError>;

    /// CL → EL: would this peer-proposed block execute cleanly?
    async fn validate_payload(
        &self,
        block: &ExecutedBlock,
    ) -> Result<PayloadStatus, BridgeError>;

    /// CL → EL: finalize this block. Fire-and-forget; failure halts the chain.
    async fn commit(&self, block_hash: BlockHash) -> Result<(), BridgeError>;
}

#[derive(Debug, Error)]
pub enum BridgeError {
    #[error("execution layer rejected payload: {0}")]
    Rejected(String),

    #[error("execution layer is syncing")]
    Syncing,

    #[error("internal: {0}")]
    Internal(#[from] eyre::Report),
}

Step 3: signature の必然性(BFT round のタイムライン)

──[ 前 round 投票中、proposer は自分のターンを準備 ]──────────────
   CL ──( build_payload(parent, attrs) )──► EL ─ 裏でブロック構築開始(投票と並走)

──[ 自分が proposer になった瞬間 — hot path ]──────────────────
   CL ──( payload_ready(id) )──► EL
   CL ◄─( ExecutedBlock を返す )── EL          ← 唯一 EL→CL にデータ逆流(予測の答え)
   CL ─► Proposal を broadcast

──[ 他 peer から Proposal 受信 ]───────────────────────────────
   CL ──( validate_payload(&ExecutedBlock) )──► EL
   CL ◄─( PayloadStatus: Valid/Invalid/Syncing )── EL
   CL ─► vote(賛成 / Nil / 棄権)

──[ 2/3+ Quorum → finalized ]──────────────────────────────────
   CL ──( commit(hash) )──► EL ─ 永続化、new head 確定

各 signature の要点:

  • build_payload(parent, attrs) -> PayloadId — 即 return、不透明 handle を返す。
  • payload_ready(id) -> ExecutedBlock — in-flight build 完了まで block するので async。唯一 EL → CL 方向にデータが逆流する seam(予測の答え)。
  • validate_payload(&ExecutedBlock) -> PayloadStatus&(borrowed)。inspect するだけで consume しない(consensus は同じブロックを複数回 inspect する)。
  • commit(BlockHash) -> Result<()> — 最小 signature、fire-and-forget。&ExecutedBlock を取らない(commit 時点で EL は既にブロックを見ており、hash だけで CL は stateless を保てる)。失敗は chain halt(レッスン9)。

Step 4: BridgeError の 3 variant

  • Rejected(String) — EL が「bad」と判定。consensus は nil 投票して次 round へ。String が human-readable な理由。
  • Syncing — EL がまだ state を持たず答えられない(bad とは別)。後でリトライ、nil 投票にはしない。
  • Internal(eyre::Report) — 予期せぬ破綻(disk full 等)。consensus は halt#[from]?eyre::Report を自動ラップ。

PayloadStatus::Syncing(status)と BridgeError::Syncing(error)の二層: 前者は「call は完了し sync state を report した」、後者は「call そのものが完了できなかった」(build_payload / commit でよく出る)。

Step 5: crate に組み込む

crates/consensus/src/lib.rs:

//! Consensus layer — Malachite BFT.

pub mod bridge;

答え合わせ

cd ~/code/openhl-reference && git checkout 13113db
diff -u ~/code/my-openhl/crates/consensus/src/bridge.rs ./crates/consensus/src/bridge.rs
diff -u ~/code/my-openhl/crates/consensus/Cargo.toml ./crates/consensus/Cargo.toml
git checkout main

doc-comment の言い回しは違って OK。4 method signature・3 error variant・#[async_trait]: Send + Sync は完全一致が必要。

合格基準

cargo check -p openhl-consensus
cargo check --workspace

→ Finished(unused 警告は OK、hard error は不可)。impl はまだない(レッスン4 から)。trait と error type だけ。

まとめ(3行)

  • 契約はちょうど 4 メソッド — BFT round 構造(propose → vote → decide)が数を決める。
  • build_payloadpayload_ready の分離が sub-second block time を支える latency trick。
  • error は 3 variant(Rejected / Syncing / Internal)= consensus の 3 応答(否決 / 待つ / 停止)。