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).Contexttrait. Decouples Malachite from the host application.Contextprovides: 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
Contexttrait → understand the host interface → readDriverfor the state machine → readVoteKeeperfor 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:
| Component | Responsibility |
|---|---|
| Driver | The orchestrator. Receives messages, dispatches to vote keeper + RSM. |
| Vote Keeper | Counts votes. Decides when 2f+1 is reached. |
| Round State Machine | The 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:
- NewRound → enter the round, decide if you're the proposer
- Propose → if proposer, broadcast a block. If not, wait.
- 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").
- Precommit → if you saw a polka, broadcast precommit. With 2f+1 precommits, block is committed.
- 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.rsand trace throughadd_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
Outputactions: "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
- Clone
informalsystems/malachite - Open
code/crates/driver/src/driver.rsand find the mainprocessmethod - Trace a single vote through: arrives → Vote Keeper → polka detected → RSM step → output
- 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.