MonadBFT Finality & Transaction Correlation
MonadBFT Finality Model
Monad uses MonadBFT, a consensus protocol with single-slot finality (~800 ms).
Consensus Stages
┌────────────┐
┌──────────│ Proposed │──────────┐
│ └─────┬──────┘ │
│ │ │
│ ┌─────▼──────┐ │
│ │ Voted │ │
│ │ (QC) │──────────┤
│ └─────┬──────┘ │
│ │ │
│ ┌─────▼──────┐ │
│ │ Finalized │ │
│ │ (commit) │──────────┤
│ └─────┬──────┘ │
│ │ │
│ ┌─────▼──────┐ ┌─────▼──────┐
│ │ Verified │ │ Rejected │
│ │ (terminal) │ │ (terminal) │
│ └────────────┘ └────────────┘
│ ▲
└────────────────────────────────┘
(rejected from any stage)
| Stage | Trigger event | Latency | Guarantee |
|---|---|---|---|
| Proposed | BlockStart | ~0 ms | None — speculative |
| Voted | BlockQC | ~400 ms | Speculative finality (2/3+ validators) |
| Finalized | BlockFinalized | ~800 ms | Full finality — irreversible |
| Verified | BlockVerified | After finalization | State root verification (terminal) |
| Rejected | BlockReject | Varies | Block discarded (terminal) |
Stage Gating (min_stage)
{"subscribe": {"events": ["TxnLog"], "min_stage": "Finalized"}}
- Events from blocks below the specified stage are discarded (not buffered for later delivery)
- Events from blocks at or above the specified stage are delivered immediately
- Events with
commit_stage = nullare delivered (fail-open)
Choosing the Right min_stage
min_stage | Latency | Risk | Best for |
|---|---|---|---|
"Proposed" (default) | ~0 ms | Block may be rejected | Real-time dashboards, monitoring, speculative UIs |
"Voted" | ~400 ms | Very unlikely to be rejected (2/3+ validators agreed) | DEX trades, semi-critical notifications |
"Finalized" | ~800 ms | None — irreversible | Bridges, exchanges, payment processors |
"Verified" | >800 ms | None — state root verified | Proof verification, cross-chain state proofs |
Trade-off: Lower min_stage = lower latency but higher risk of processing events from rejected blocks. Higher min_stage = guaranteed finality but added latency.
The commit_stage Field
Every event carries a commit_stage field: the block's consensus stage at the moment the event was serialized for broadcast (not the block's current live stage). This is a frozen snapshot:
- Live stream: Almost always
"Proposed". Execution events are written immediately at block execution, before any consensus votes - Resume/replay from ring buffer: May be
"Voted","Finalized", or even"Verified". The block progressed through consensus while the message sat in the buffer waiting to be replayed null: Block is not in the lifecycle tracker (rare edge case). Delivered as fail-open
Important: To know the current stage of a block, do not rely on
commit_stage. Subscribe to Lifecycle events instead.
Pattern: Buffering + Lifecycle
For applications requiring finality (bridge, exchange):
- Subscribe to the events you need +
Lifecycle - Buffer events by
block_number - When Lifecycle confirms
to_stage: "Finalized", process the buffer - If
to_stage: "Rejected", discard the buffer
Transaction Correlation
With "correlate": true in the extended subscription, if a TxnHeaderStart passes the filters (sender/to/function_selector), the server automatically delivers all subsequent events for that transaction.
How It Works
| Event | Action |
|---|---|
BlockStart / BlockEnd | Reset the correlation set |
TxnHeaderStart | If it passes the filter → add txn_idx to the set |
Any event with txn_idx | If txn_idx is in the set → deliver (bypassing the normal filter) |
TxnEnd | If txn_idx is in the set → deliver, remove from set |
What Is Auto-Delivered
With correlate enabled, for a matching transaction you will receive:
TxnHeaderStart(passed the filter)TxnLog(all transaction logs)TxnCallFrame(all internal calls)TxnEvmOutput(execution result)TxnEnd(completion)
Example: Tracking delegate() on the Staking Contract
{
"subscribe": {
"events": ["TxnHeaderStart"],
"filters": [{
"event_name": "TxnHeaderStart",
"field_filters": [
{"field": "to", "filter": {"values": ["0x0000000000000000000000000000000000001000"]}},
{"field": "function_selector", "filter": {"values": ["0x84994fec"]}}
]
}],
"correlate": true
}
}
Result: the full chain TxnHeaderStart → TxnLog → TxnEvmOutput → TxnEnd for each delegate() call.