FABRKNT
Consensus Engineering — Reth で L1 のコンセンサスを作る
実コンセンサスコードを読む
レッスン 6 / 12·CONTENT18 分45 XP
コース
Consensus Engineering — Reth で L1 のコンセンサスを作る
レッスンの役割
CONTENT
順序
6 / 12

レッスン5 — Malachite を読む(Informal Systems の Rust-native BFT)

問い

Reth ベース L1 が Tendermint 系 BFT を必要とするとき、選択肢は 3 つ。① 自前で書く(数か月 + セキュリティリスク) ② CometBFT(Go)にクロスプロセスで委譲(醜い糊コード) ③ informalsystems/malachite を使う(CometBFT を作ったのと同じチームによる Tendermint の Rust 書き直し)。選択肢 3 のアーキテクチャはどうなっているか?

原理(最小モデル)

  • 3 つの差し替え可能なレイヤ. Driver(オーケストレータ)+ Vote Keeper(quorum ロジック)+ Round State Machine(Tendermint ルール)。
  • 3 ラウンドの投票. Propose → Prevote → Precommit。2 ラウンド目(Precommit)が「2f+1 が同じブロックに合意」を保証 — 1 ラウンドだけだと Byzantine リーダーが split-vote を仕掛けて commit を混乱させられる。
  • Application は Context trait で接続. 自分のブロック型 / validator set / 署名方式を提供すれば、Malachite Driver がプロトコルすべてを処理する。
  • 本番事例 = Astria. Reth ベース共有 sequencer + CometBFT、Malachite に置き換え可能。

具体例

3 レイヤのアーキテクチャ:

flowchart TB
    App["Application<br/>(自分の chain)"] -->|propose/validate| Driver["Driver<br/>(オーケストレータ)"]
    Driver -->|Vote keeper| VK["Vote Keeper<br/>(quorum ロジック)"]
    Driver -->|Round state machine| RSM["Round State Machine<br/>(Tendermint ルール)"]
    VK -->|2f+1 達成?| Driver
    RSM -->|次ステップ| Driver

Round State Machine:

pub enum Step {
    NewRound,
    Propose,
    Prevote,
    Precommit,
    Commit,
}

各ラウンドの遷移:

  1. NewRound → ラウンドに入り、自分が proposer かを判定
  2. Propose → proposer ならブロックをブロードキャスト、違うなら待つ
  3. Prevote → 提案ブロックに「yes」または「nil」を投票。同じブロックへの 2f+1 prevote = polka
  4. Precommit → polka を見たら precommit をブロードキャスト。2f+1 precommit で commit
  5. Commit → 確定、次の height へ

Vote Keeper:

pub struct VoteKeeper<Ctx: Context> {
    height: Ctx::Height,
    threshold: ThresholdParam,
    rounds: BTreeMap<Round, RoundVotes<Ctx>>,
}

impl<Ctx: Context> VoteKeeper<Ctx> {
    pub fn add_vote(&mut self, vote: Ctx::Vote, weight: Weight) -> VoteKeeperOutput<Ctx::Value>;
    pub fn get_polka(&self, round: Round) -> Option<Ctx::Value>;
    pub fn get_commit(&self, round: Round) -> Option<Ctx::Value>;
}

操作は 3 つ — 票の追加、polka 確認、commit 確認。それだけ。Tendermint の quorum ロジックが 1 つの struct に収まる。

Driver の I/O:

pub enum Input<Ctx: Context> {
    NewHeight(Ctx::Height, ValidatorSet),
    Propose(Ctx::Proposal),
    Vote(Ctx::Vote),
    TimeoutElapsed(Timeout),
}

pub enum Output<Ctx: Context> {
    Propose(Ctx::Value),
    Vote(Ctx::Vote),
    Decide(Ctx::Height, Round, Ctx::Value),
    ScheduleTimeout(Timeout),
}

Context trait(application 側が実装):

pub trait Context {
    type Address;
    type Height;
    type Vote: Vote<Self>;
    type Proposal;
    type Value;  // = 自分のブロック型
    type ValidatorSet;
    type SigningScheme;
    // ...
}

Tempo クラスの統合例:

以下は概念スニペット(associated type と配線の形を示すための抜粋)。

struct TempoContext;
impl Context for TempoContext {
    type Address = ValidatorAddress;
    type Height = BlockNumber;
    type Vote = TempoVote;
    type Proposal = TempoBlock;      // Reth 互換 Block
    type Value = BlockHash;
    type ValidatorSet = TempoValidatorSet;
    type SigningScheme = Ed25519;
}

// メインループ
let mut driver = Driver::<TempoContext>::new(/* params */);
loop {
    let input = network.next_message().await;
    let outputs = driver.process(input);
    for output in outputs {
        handle(output).await;
    }
}

失敗例(誤解)

「投票が 1 ラウンドで十分」— 間違い。1 ラウンドだけだと Byzantine リーダーが split-vote を仕掛けられる(異なるバリデータに異なるブロックを送り、各バリデータが違うブロックに投票し、後の commit が混乱)。2 ラウンド目(Precommit)が「2f+1 が同じブロックに合意した」ことを確認 — これが safety を保証する。

「Malachite は CometBFT より速い」— 比較できない。同じプロトコル(Tendermint)の Rust 実装で、Astria のような Rust chain に組み込みやすいのが利点。性能比較ではなく 言語適合性 の選択。

🛑 予測。 Tendermint はブロックあたり 3 つの投票ラウンド(Propose / Prevote / Precommit)を持つ。各ラウンドで、バリデータは何を決定するのか? 各ラウンドの入力と出力を列挙。(答え: ① Propose: 入力 = リーダーの提案、出力 = 「これに賛成 / 反対」の準備、② Prevote: 入力 = 全バリデータの prevote、出力 = polka(同じブロックへの 2f+1 prevote)の検知、③ Precommit: 入力 = polka 観測 + 全バリデータの precommit、出力 = commit 決定(2f+1 precommit)。)

ステップで組み立てる

Step 1: 3 レイヤを言える

  • Driver = オーケストレータ(メッセージ受け取り → ディスパッチ → output 発火)
  • Vote Keeper = quorum 集計(add_vote / get_polka / get_commit)
  • Round State Machine = Tendermint プロトコル(5 状態の遷移)

Step 2: 投票 2 ラウンドの必要性を 1 文で

「2 ラウンド目(Precommit)は 2f+1 が同じブロックに合意した ことを確認するため。1 ラウンドだけだと Byzantine リーダーの split-vote を許す。」

Step 3: Application 側の Context 実装

自分の chain で実装するのは Context trait の associated types のみ:

  • ブロック型(Reth の Block
  • Validator set(カスタム struct)
  • 署名方式(ECDSA / BLS / Ed25519)

あとは Driver がすべて処理する。

Step 4: リポで確認

code/crates/vote/src/lib.rs を開いて add_vote を追う。何が何で加重されているか? 2f+1 の閾値はどこから来ているか?

答え合わせ

  • Astria の現状: Reth ベース共有 sequencer + CometBFT(Go 版 Tendermint)で本番運用中。Malachite に置き換え可能 = 同プロトコルの Rust 実装。
  • Malachite が解放してくれるもの: Driver / Vote Keeper / RSM のすべて。Tendermint プロトコルを自分で書かずに済む。Application は primitives(block 型、validator set、署名)だけ提供すればよい。
  • 本番までの距離: Astria を見ながら Reth ↔ Malachite の Engine API 配線をなぞれば、Tempo クラスの L1 chain が組み立てられる。

合格基準

  • 3 レイヤ(Driver / Vote Keeper / RSM)の役割を即答できる。
  • 5 状態(NewRound → Propose → Prevote → Precommit → Commit)の遷移を辿れる。
  • Context trait に最低限必要な 5 つの associated type を言える。
  • polka と commit の違いを 1 文で言える(polka = 2f+1 prevote、commit = 2f+1 precommit)。

まとめ(3行)

  • Malachite = Tendermint の Rust 書き直し(CometBFT の元チームによる)、Reth ベース chain への組み込みが最もクリーンな選択肢。
  • 3 レイヤ(Driver / Vote Keeper / RSM)+ Context trait で application が接続。Driver がプロトコルすべてを処理する。
  • 3 投票ラウンド(Propose / Prevote / Precommit)の 2 ラウンド目が「2f+1 が同じブロックに合意」を保証 — split-vote 攻撃を潰す。