tenzro-consensus
HotStuff-2 BFT consensus engine with reputation-weighted proposer election, no-endorsement certificates, and hybrid post-quantum signatures. Powers the Tenzro Ledger L1 settlement layer.
For the full protocol specification with formal arguments and references, see docs/papers/tenzro-consensus.md.
Overview
The crate composes four BFT mechanisms:
- Two-phase HotStuff-2 — linear-communication, partially-synchronous BFT with 2Δ optimistic finality.
- Reputation-weighted proposer election — stake × observed-behaviour leader draw. Closes the small-validator-set stall mode that round-robin suffers when one validator is flaky.
- No-endorsement certificates (NECs) — closes the tail-fork attack class on 2-chain HotStuff.
- Ed25519 + ML-DSA-65 hybrid signatures — every safety-critical message (vote, timeout, no-endorsement) is hybrid-signed. NIST FIPS 204 compliant.
For academic citations and the formal protocol specification, see docs/papers/tenzro-consensus.md.
Modules
| File | Lines | Role |
|---|---|---|
lib.rs |
231 | Public surface, type re-exports |
config.rs |
211 | ConsensusConfig, BftThreshold, ProposerElectionKind |
hotstuff2.rs |
3,562 | The HotStuff-2 state machine |
validator.rs |
736 | ValidatorSet, ProposerElection trait |
proposer.rs |
400 | ReputationProposer, RoundRobinProposer |
leader_reputation.rs |
855 | LeaderReputation engine, weight computation, anti-grinding seed |
timeout.rs |
1,710 | TimeoutMsg / TimeoutCertificate (TC) and NoEndorsementMsg / NoEndorsementCertificate (NEC) |
vote_state.rs |
570 | EquivocationDetector |
voter.rs |
724 | Vote, QuorumCertificate, VoteCollector |
mempool.rs |
875 | Transaction admission + ordering |
admission.rs |
622 | Lane-based fee floor admission |
epoch_manager.rs |
574 | Atomic epoch transitions |
finality.rs |
502 | 2-chain finality tracker |
traits.rs |
123 | ConsensusEngine, SlashingCallback, ConsensusOutMessage |
error.rs |
140 | ConsensusError |
11,835 LOC total.
Protocol
Two-phase HotStuff-2
Each view runs Prepare → Commit. A block is finalized once its child has formed a Commit QC at the next view (the 2-chain rule). Leader → replicas → leader vote flow keeps communication linear in the optimistic path.
Reputation-weighted proposer election
Each round draws the leader from a stake-weighted seeded distribution where per-validator weight is multiplied by an observed-behaviour tier:
weight(v) = stake(v) × tier(v) × tee_multiplier(v) / 10000
tier(v):
ACTIVE_WEIGHT = 1000 if v proposed ≥1 QC-certified block recently
and failed <10% of its proposer-window rounds
INACTIVE_WEIGHT = 10 if v voted but didn't propose
FAILED_WEIGHT = 1 otherwise
tee_multiplier(v):
15000 (1.5×) if v has a fresh valid TEE attestation in the current epoch
10000 (1.0×) otherwise
The 1000× spread between ACTIVE and FAILED collapses a chronically-flaky validator's effective draw probability to ~0.1% within ~20 rounds, long before its degradation propagates into chain-wide liveness loss.
The leader-draw seed is anti-grinding:
seed = SHA-256(
"TENZRO_LEADER_REPUTATION:"
|| epoch.to_be_bytes()
|| round.to_be_bytes()
|| prev_finalized_block_id
)
prev_finalized_block_id is fixed at least one full QC ago, and the proposer-history window excludes the most recent 20 rounds (TRAILING_BUFFER_ROUNDS = 20), so an adversary leader cannot grind candidate blocks to bias future draws.
TEE multiplier (1.5×, multiplicative)
A validator with a fresh, valid TEE attestation receives a 1.5× multiplier on its reputation-adjusted weight. The multiplicative form (rather than a hard 2× boost) preserves the property that observed behaviour can fully overcome attestation: a TEE-attested FAILED validator (weight = stake × 1 × 1.5) is still dwarfed by a non-TEE ACTIVE validator (weight = stake × 1000 × 1).
No-Endorsement Certificates (NEC)
Closes the tail-fork attack on 2-chain HotStuff. The leader at view v must either:
- Re-propose the high-tip from view v−1, or
- Attach a valid NEC for view v
A NEC is a f+1 aggregation of NoEndorsementMsgs, each attesting "I observed no QC at view v−1". f+1 (not 2f+1) is the correct threshold: with at most f Byzantine signers, f+1 suffices to guarantee at least one truthful "no QC observed" attestation. Domain tag TENZRO_NO_ENDORSEMENT: distinct from the timeout and vote tags prevents cross-message replay.
Hybrid post-quantum signatures
Every safety-critical message carries a CompositeSignature = Ed25519 || ML-DSA-65. Verification is AND-composed — forging requires breaking both schemes. A quantum adversary that breaks Ed25519 (Shor's algorithm on EC discrete log) still cannot forge, because ML-DSA-65 is conjectured quantum-secure.
| Message | Domain tag |
|---|---|
| Vote | TENZRO_VOTE: |
| TimeoutMsg | TENZRO_TIMEOUT: |
| NoEndorsementMsg | TENZRO_NO_ENDORSEMENT: |
| Block proposal | TENZRO_BLOCK: |
| Reputation seed | TENZRO_LEADER_REPUTATION: |
Byzantine fault tolerance
Tolerates up to f faulty validators where f = ⌊(n−1)/3⌋. Quorum is 2f+1. With n = 4: f = 1, quorum = 3. With n = 100: f = 33, quorum = 67.
Equivocation detection and slashing
The EquivocationDetector (in vote_state.rs) tracks per-validator votes per view. Two distinct votes from the same validator at the same view trigger:
StakingSlashingCallback::report_equivocation(validator, view, evidence) {
slash_amount = stake.amount / 10; // 10%
staking.slash(validator, slash_amount, reason, ...);
epoch_manager.remove_pending_validator(validator);
}
Pipeline: EquivocationDetector → SlashingCallback trait → tenzro-node::StakingSlashingCallback → tenzro-token::StakingManager::slash → on-chain TNZO burn. Slashed validators are dropped from the next epoch's pending queue.
View change (DiemBFT pacemaker)
On local view timeout, replicas broadcast a signed TimeoutMsg(view, high_qc_view). Receivers observing a higher-view timeout advance their local view. 2f+1 timeouts at the same view aggregate into a Timeout Certificate (TC). The next leader attaches the TC to its proposal.
If view v−1 produced no QC, replicas additionally broadcast NoEndorsementMsg(view=v). f+1 of these aggregate into a NEC.
Configuration
use ;
let config = default
.with_block_time // 400ms block time
.with_view_timeout // 2s view timeout
.with_max_block_size // 2MB
.with_proposer_election; // default
Defaults:
| Parameter | Default |
|---|---|
block_time_ms |
400 |
view_timeout_ms |
2000 |
max_block_size |
2 MiB |
max_transactions_per_block |
10,000 |
max_gas_per_block |
30,000,000 |
min_validators |
4 |
bft_threshold |
TwoThirdsPlusOne |
epoch_duration |
10,000 blocks |
proposer_election |
Reputation |
optimistic_responsiveness |
true |
ProposerElectionKind::RoundRobin is retained for tests and replay benchmarks.
Usage
use ;
use ;
use MlDsaSigningKey;
use BlsKeyPair;
async
Performance
Per consensus message, hybrid signing cost:
| Operation | Time |
|---|---|
| Sign (Ed25519 + ML-DSA-65) | ~300 µs |
| Verify | ~280 µs |
| Signature size | 3,373 bytes |
| Public key size | 1,984 bytes |
For n = 100 validators:
- QC verification (2f+1 = 67 hybrid checks): ~18.7 ms
- One block round (intra-region): ~40 ms
- Theoretical ceiling: ~25 blocks/sec
For n = 4 (current testnet, single-zone):
- QC verification (3 hybrid checks): ~0.84 ms
- Observed: ~10 blocks/sec empty-block finalization
Real TPS is execution-layer-dependent and orthogonal to consensus throughput. Production HotStuff-2 deployments at n ≈ 150 sustain in the range of 5,000–12,000 TPS depending on execution layer and block size; we expect Tenzro to land in the same operating range when deployed at comparable validator counts.
Safety and liveness
- Safety. No two honest replicas finalize different blocks at the same height. Follows from HotStuff-2's two-chain rule.
- Liveness (after GST). Progress guaranteed once 2f+1 honest validators exchange messages within bounded delay. Follows from the DiemBFT v4 pacemaker argument.
- Single-validator-fault resilience. Reputation election deprioritizes flaky validators within ~20 rounds.
- Tail-fork resistance. NEC blocks fresh-block proposals after a timeout unless f+1 validators attest no QC was observed.
- Quantum forgery resistance. Hybrid Ed25519 + ML-DSA-65 over every safety-critical signature.
Test coverage
39 unit tests covering:
- Reputation weight computation (16 tests)
- Anti-grinding seed determinism and domain separation
- Window edge cases (genesis, post-rollover)
- Stake-weighted draw distribution
- TC aggregation and verification
- NEC aggregation and verification
- Cross-message replay rejection (signing payload tag binding)
- Equivocation detection
- Epoch transition atomicity
- Hybrid signature verification (rejecting either-half-only signatures)
Dependencies
tenzro-types,tenzro-crypto— primitives, hybrid signingtokio,async-trait— async runtimedashmap,parking_lot— concurrent stateserde,serde_json,bincode— serializationtracing— logging
License
Apache License 2.0 — see LICENSE.