use crate::{
quorum::{AddResult, QuorumCert, VoteAccumulator},
ValidatorSet, Vote, VoteType,
};
use axiom_core::{
config::ChainConfig,
types::{Block, Height, Round},
};
use axiom_crypto::Hash;
use parking_lot::Mutex;
use std::sync::Arc;
use tracing::{debug, info, warn};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Phase {
Propose,
Prevote,
Precommit,
Commit,
}
#[derive(Debug)]
struct RoundState {
round: Round,
phase: Phase,
proposal: Option<Block>,
prevotes: VoteAccumulator,
precommits: VoteAccumulator,
locked_hash: Option<Hash>,
locked_round: Option<Round>,
}
impl RoundState {
fn new(round: Round) -> Self {
RoundState {
round,
phase: Phase::Propose,
proposal: None,
prevotes: VoteAccumulator::default(),
precommits: VoteAccumulator::default(),
locked_hash: None,
locked_round: None,
}
}
}
#[derive(Debug)]
pub enum ConsensusEvent {
Committed { height: Height, block: Block, qc: QuorumCert },
NeedProposal { height: Height, round: Round },
RoundTimeout { height: Height, round: Round },
Equivocation { validator: String, height: Height },
}
pub struct Engine {
config: ChainConfig,
validator_set: ValidatorSet,
inner: Mutex<EngineInner>,
}
struct EngineInner {
height: Height,
round_state: RoundState,
committed: Vec<(Height, Hash)>,
}
impl Engine {
pub fn new(config: ChainConfig, validator_set: ValidatorSet, start_height: Height) -> Self {
let inner = EngineInner {
height: start_height,
round_state: RoundState::new(0),
committed: Vec::new(),
};
Engine { config, validator_set, inner: Mutex::new(inner) }
}
pub fn height(&self) -> Height {
self.inner.lock().height
}
pub fn round(&self) -> Round {
self.inner.lock().round_state.round
}
pub fn receive_proposal(&self, block: Block) -> Option<ConsensusEvent> {
let mut inner = self.inner.lock();
let height = inner.height;
let round = inner.round_state.round;
let phase = inner.round_state.phase;
if phase != Phase::Propose {
debug!("ignoring late proposal in phase {:?}", phase);
return None;
}
if block.height() != height {
warn!("proposal height mismatch: {} vs {}", block.height(), height);
return None;
}
info!(height, round, hash = %block.hash(), "received proposal");
inner.round_state.proposal = Some(block);
inner.round_state.phase = Phase::Prevote;
None
}
pub fn receive_vote(&self, vote: Vote) -> Option<ConsensusEvent> {
let mut inner = self.inner.lock();
let threshold = self.config.quorum_threshold(self.validator_set.total_power());
let height = inner.height;
let round = inner.round_state.round;
if vote.height != height || vote.round != round {
debug!("ignoring vote for h={} r={}", vote.height, vote.round);
return None;
}
let rs = &mut inner.round_state;
match vote.vote_type {
VoteType::Prevote => {
if rs.add_prevote(vote, &self.validator_set) == AddResult::Added {
if let Some(hash) = rs.prevote_quorum_hash(&self.validator_set, threshold) {
info!(height, round, %hash, "prevote quorum reached");
rs.locked_hash = Some(hash);
rs.locked_round = Some(round);
rs.phase = Phase::Precommit;
}
}
}
VoteType::Precommit => {
let block_hash = vote.block_hash;
if rs.add_precommit(vote, &self.validator_set) == AddResult::Added {
if let Some(hash) = block_hash {
if rs.precommits.has_quorum(Some(hash), &self.validator_set, threshold) {
let qc = QuorumCert::try_build(
&rs.precommits,
height,
round,
hash,
&self.validator_set,
threshold,
)?;
let block = rs.proposal.clone()?;
info!(height, %hash, "precommit quorum — committing block");
inner.committed.push((height, hash));
inner.height += 1;
inner.round_state = RoundState::new(0);
return Some(ConsensusEvent::Committed { height, block, qc });
}
}
}
}
}
None
}
pub fn timeout(&self) -> ConsensusEvent {
let mut inner = self.inner.lock();
let height = inner.height;
let old_round = inner.round_state.round;
let new_round = old_round + 1;
inner.round_state = RoundState::new(new_round);
warn!(height, old_round, new_round, "consensus timeout — advancing round");
ConsensusEvent::RoundTimeout { height, round: old_round }
}
pub fn committed_blocks(&self) -> Vec<(Height, Hash)> {
self.inner.lock().committed.clone()
}
}
impl RoundState {
fn add_prevote(&mut self, vote: Vote, vset: &ValidatorSet) -> AddResult {
self.prevotes.add(vote, vset)
}
fn add_precommit(&mut self, vote: Vote, vset: &ValidatorSet) -> AddResult {
self.precommits.add(vote, vset)
}
fn prevote_quorum_hash(&self, vset: &ValidatorSet, threshold: u64) -> Option<Hash> {
for h in self.prevotes.candidate_hashes() {
if self.prevotes.has_quorum(Some(h), vset, threshold) {
return Some(h);
}
}
None
}
}
pub type SharedEngine = Arc<Engine>;
#[cfg(test)]
mod tests {
use super::*;
use crate::validator_set::Validator;
use axiom_core::{
config::ChainConfig,
types::{Block, BlockHeader},
};
use axiom_crypto::{Hash, Keypair};
fn test_setup(n: usize) -> (SharedEngine, ValidatorSet, Vec<Keypair>) {
let kps: Vec<Keypair> = (0..n as u8).map(|i| Keypair::from_bytes(&[i + 1; 32])).collect();
let validators: Vec<Validator> = kps
.iter()
.map(|kp| Validator { id: kp.public_key(), voting_power: 100 })
.collect();
let vset = ValidatorSet::new(validators);
let engine = Arc::new(Engine::new(ChainConfig::default(), vset.clone(), 1));
(engine, vset, kps)
}
fn make_block(height: u64, proposer: &Keypair) -> Block {
let header = BlockHeader {
chain_id: "axiom-devnet-1".into(),
height,
epoch: 0,
round: 0,
timestamp: 0,
parent_hash: Hash::ZERO,
tx_root: Hash::ZERO,
state_root: Hash::ZERO,
quorum_cert: Hash::ZERO,
proposer: proposer.public_key(),
};
let sig = proposer.sign(header.hash().as_bytes());
Block { header, transactions: vec![], proposer_sig: sig }
}
#[test]
fn full_consensus_round() {
let (engine, _vset, kps) = test_setup(4);
let block = make_block(1, &kps[0]);
let block_hash = block.hash();
engine.receive_proposal(block.clone());
for kp in kps.iter().take(3) {
let v = Vote::sign(VoteType::Prevote, 1, 0, Some(block_hash), kp);
engine.receive_vote(v);
}
let mut result = None;
for kp in kps.iter().take(3) {
let v = Vote::sign(VoteType::Precommit, 1, 0, Some(block_hash), kp);
result = engine.receive_vote(v);
}
assert!(matches!(result, Some(ConsensusEvent::Committed { height: 1, .. })));
assert_eq!(engine.height(), 2);
}
}