FABRKNT
Consensus Engineering — Building L1 Consensus on Reth
Reading real consensus code
Lesson 6 of 12·CONTENT18 min45 XP

Treat this page as a workbench, not a blog post. The goal is to extract a reusable mental model from the source and carry it into the rest of the Fabrknt stack.

Course
Consensus Engineering — Building L1 Consensus on Reth
Lesson role
CONTENT
Sequence
6 / 12

Lesson 6 — Reading Malachite — Rust-native BFT by Informal Systems

Question

Malachite is a Rust-native Tendermint-style BFT implementation by Informal Systems. Read it line by line — Driver + VoteKeeper + Context — to understand how a production BFT protocol is actually structured. The same patterns appear in HyperBFT, HotStuff, and every classical BFT.

Principle (minimum model)

  • Driver. Orchestrates the consensus round. Drives state transitions: propose → prevote → precommit → commit / round-change.
  • VoteKeeper. Tallies votes and detects quorum (2/3 stake-weighted). Handles equivocation (two votes for the same height/round from one validator).
  • Context trait. Decouples Malachite from the host application. Context provides: validator set + propose function + execute function. Host plugs in its specifics.
  • Round state machine. Each height has multiple rounds; each round has multiple steps (propose / prevote / precommit). Timeouts trigger round-change.
  • Why these abstractions. Same shape works for Ethereum PoS / Cosmos Tendermint / Hyperliquid HyperBFT / any classical BFT. The patterns generalise.
  • Production use. Cosmos chains use a Go Tendermint; Malachite is the Rust equivalent. Informal Systems built it as part of their work on the Cosmos ecosystem.
  • Reading order. Start with Context trait → understand the host interface → read Driver for the state machine → read VoteKeeper for vote tallying.

Worked example + steps

Reading Malachite — Rust-native BFT by Informal Systems

If your Reth-based L1 needs Tendermint-style BFT consensus, you have three options. (1) Write your own — months of work plus security risk. (2) Shell out to CometBFT in Go — ugly cross-process glue. (3) Use informalsystems/malachite: Tendermint, rewritten in Rust, by the same team that built CometBFT. Option 3 is the reason this lesson exists.

Malachite is the closest Rust-native BFT engine you can study, and the cleanest one to embed.

1. Why Malachite exists

Informal Systems built CometBFT (Go). They watched the ecosystem ship Tendermint-derivative chains for years. They concluded:

  • Rust-native consensus is now standard (Reth, Lighthouse-as-rewrite, etc.)
  • The Go reference is hard to embed in Rust chains
  • A new high-quality Rust BFT engine would be a public good

Malachite is the result. Architecturally faithful to Tendermint, ergonomically Rust.

The repo: code/ is the main implementation.

2. The core architecture

Three pluggable layers:

flowchart TB
    App["Application<br/>(your chain)"] -->|propose/validate| Driver["Driver<br/>(orchestrator)"]
    Driver -->|Vote keeper| VK["Vote Keeper<br/>(quorum logic)"]
    Driver -->|Round state machine| RSM["Round State Machine<br/>(Tendermint rules)"]
    VK -->|2f+1 met?| Driver
    RSM -->|next step| Driver

Three components, clean separation:

ComponentResponsibility
DriverThe orchestrator. Receives messages, dispatches to vote keeper + RSM.
Vote KeeperCounts votes. Decides when 2f+1 is reached.
Round State MachineThe actual Tendermint protocol — state transitions, view changes.

Your application plugs in at the top — Malachite calls back to you for "propose this block" and "validate this block."

3. The Round State Machine — Tendermint rules

The heart of Tendermint, captured in code:

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

Each block round, validators transition through these:

  1. NewRound → enter the round, decide if you're the proposer
  2. Propose → if proposer, broadcast a block. If not, wait.
  3. Prevote → vote "yes" or "nil" on the proposed block. 2f+1 prevotes for the same block is called a polka (Tendermint's name for "enough first-round support to move on").
  4. Precommit → if you saw a polka, broadcast precommit. With 2f+1 precommits, block is committed.
  5. Commit → finalize, move to next height

Two thirds of validators must vote in each round for progress. If they don't, view changes happen.

The second round confirms that 2f+1 validators agreed on the same block, not just that they voted. If you had only one round, a Byzantine leader could split-vote and confuse downstream commits. The two-round structure is the safety guarantee.

4. The Vote Keeper — quorum logic

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

The Vote Keeper counts votes per round and tracks:

  • Per round: prevotes for each candidate block, precommits for each candidate block
  • Polkas: when 2f+1 prevotes accumulate for a block
  • Commits: when 2f+1 precommits accumulate for a block

It exposes:

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>;
}

Three operations: add a vote, check for polka, check for commit. That's it. All of Tendermint's quorum logic is in this struct.

🔍 Find in repo. Open code/crates/vote/src/lib.rs and trace through add_vote. What weighted by what? Where does the 2f+1 threshold come from?

5. The Driver — orchestrator

The Driver receives messages (proposals, votes), runs them through the Vote Keeper and RSM, and emits outputs (actions for the application to take):

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),
}

This is the entire interface between the protocol engine and your application:

  • App calls Driver.process(Input) when network messages arrive
  • Driver returns Output actions: "propose this," "vote on that," "I decided, commit this block"

Clean event-loop pattern. Your app calls this in a hot loop.

6. The Application interface — what you implement

To wire Malachite to Reth:

pub trait Context {
    type Address;
    type Height;
    type Vote: Vote<Self>;
    type Proposal;
    type Value;  // = your block type
    type ValidatorSet;
    type SigningScheme;
    // ...
}

You implement Context with your block type (Reth's Block), your validator set (your custom struct), your signature scheme (ECDSA, BLS, etc.).

Then Malachite's Driver handles everything: rounds, voting, timeouts, view changes. You provide just the application primitives.

7. Astria — a real Malachite consumer on Reth

astriaorg/astria ships a shared sequencer that uses CometBFT (Go Tendermint) for consensus over Reth-based rollups. They could swap to Malachite — same protocol, different implementation language.

This is the deployment shape:

Application (Reth-based rollup) ←→ Sequencer (CometBFT/Malachite) ←→ Validator set

Astria is the cleanest open-source example of "Reth + BFT consensus" in production. Worth reading.

8. For your Tempo-style L1

If you ship a Tempo-class L1 with Malachite:

// Your context
struct TempoContext;
impl Context for TempoContext {
    type Address = ValidatorAddress;
    type Height = BlockNumber;
    type Vote = TempoVote;       // typed vote struct
    type Proposal = TempoBlock;   // your reth-compatible Block
    type Value = BlockHash;
    type ValidatorSet = TempoValidatorSet;
    type SigningScheme = Ed25519;
    // ...
}

// Main loop
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;
    }
}

That's it at the architecture level. Malachite handles the protocol; you handle Reth integration via the consensus trait you saw last lesson.

9. Practice

  1. Clone informalsystems/malachite
  2. Open code/crates/driver/src/driver.rs and find the main process method
  3. Trace a single vote through: arrives → Vote Keeper → polka detected → RSM step → output
  4. Identify exactly where 2f+1 is checked (it's one specific function — find it)

Final check: in one sentence, what does Malachite let you avoid writing yourself? If you can't articulate it, you haven't internalized the value of having a Rust BFT engine ready to embed.

Summary (3 lines)

  • Malachite = Rust-native Tendermint-style BFT. Three core abstractions: Driver (state machine) + VoteKeeper (vote tallying + equivocation detection) + Context (host interface).
  • Same patterns work for Ethereum PoS / Cosmos Tendermint / HyperBFT / any classical BFT. Round state machine with timeout-driven round-change.
  • Reading order: Context → Driver → VoteKeeper. Production-grade Rust BFT for the Cosmos ecosystem. Next: bera-reth Proof-of-Liquidity.