Skip to content

Commit a8e8b50

Browse files
committed
feat: consensus state & vote tracking
1 parent 2078c4f commit a8e8b50

3 files changed

Lines changed: 580 additions & 11 deletions

File tree

crates/consensus/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ tokio = { workspace = true }
1212
thiserror = { workspace = true }
1313
tracing = { workspace = true }
1414
serde = { workspace = true }
15+
chrono = { workspace = true }
1516

1617
[dev-dependencies]
1718
proptest = { workspace = true }
Lines changed: 272 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,283 @@
1-
//! Consensus state machine.
1+
//! Consensus state machine for Autobahn BFT.
2+
//!
3+
//! Implements the state transitions for the two-layer Autobahn consensus:
4+
//! - Layer 1: Car creation (data dissemination)
5+
//! - Layer 2: Cut consensus (PBFT-style voting)
26
3-
/// Consensus step enum.
7+
use serde::{Deserialize, Serialize};
8+
use std::fmt;
9+
use types::{Block, Hash, Height, Round, ValidatorSet, Vote, VoteType};
10+
11+
/// Consensus step in the state machine.
12+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
413
pub enum ConsensusStep {
5-
/// New height phase.
14+
/// New height: waiting for proposal.
615
NewHeight,
7-
/// Propose phase.
16+
/// Propose: leader creates proposal.
817
Propose,
9-
/// Prepare phase.
18+
/// Prepare: validators vote on proposal.
1019
Prepare,
11-
/// Commit phase.
20+
/// Commit: validators commit to proposal.
1221
Commit,
13-
/// Finalized phase.
22+
/// Finalized: block committed and executed.
1423
Finalized,
1524
}
1625

17-
/// Consensus state structure.
26+
impl fmt::Display for ConsensusStep {
27+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28+
match self {
29+
ConsensusStep::NewHeight => write!(f, "NewHeight"),
30+
ConsensusStep::Propose => write!(f, "Propose"),
31+
ConsensusStep::Prepare => write!(f, "Prepare"),
32+
ConsensusStep::Commit => write!(f, "Commit"),
33+
ConsensusStep::Finalized => write!(f, "Finalized"),
34+
}
35+
}
36+
}
37+
38+
/// Consensus state for a specific height and round.
39+
#[derive(Debug, Clone)]
1840
pub struct ConsensusState {
19-
// TODO: Add fields
41+
/// Current blockchain height.
42+
pub height: Height,
43+
/// Current consensus round.
44+
pub round: Round,
45+
/// Current consensus step.
46+
pub step: ConsensusStep,
47+
/// Validator set for this height.
48+
pub validators: ValidatorSet,
49+
/// Proposed block (if any).
50+
pub proposal: Option<Block>,
51+
/// Hash of the locked block (from Prepare phase).
52+
pub locked_block: Option<Hash>,
53+
/// Round when we locked on a block.
54+
pub locked_round: Option<Round>,
55+
/// Hash of block we're valid for (from Commit phase).
56+
pub valid_block: Option<Hash>,
57+
/// Round when we saw valid block.
58+
pub valid_round: Option<Round>,
59+
/// Prepare votes received.
60+
pub prepare_votes: Vec<Vote>,
61+
/// Commit votes received.
62+
pub commit_votes: Vec<Vote>,
63+
/// Last committed block.
64+
pub last_commit: Option<Block>,
65+
}
66+
67+
impl ConsensusState {
68+
/// Create a new consensus state for a given height.
69+
pub fn new(height: Height, validators: ValidatorSet) -> Self {
70+
Self {
71+
height,
72+
round: Round::default(),
73+
step: ConsensusStep::NewHeight,
74+
validators,
75+
proposal: None,
76+
locked_block: None,
77+
locked_round: None,
78+
valid_block: None,
79+
valid_round: None,
80+
prepare_votes: Vec::new(),
81+
commit_votes: Vec::new(),
82+
last_commit: None,
83+
}
84+
}
85+
86+
/// Transition to a new round.
87+
pub fn enter_new_round(&mut self, round: Round) {
88+
self.round = round;
89+
self.step = ConsensusStep::NewHeight;
90+
self.proposal = None;
91+
self.prepare_votes.clear();
92+
self.commit_votes.clear();
93+
}
94+
95+
/// Transition to Propose step.
96+
pub fn enter_propose(&mut self) {
97+
self.step = ConsensusStep::Propose;
98+
}
99+
100+
/// Transition to Prepare step with a proposal.
101+
pub fn enter_prepare(&mut self, proposal: Block) {
102+
self.proposal = Some(proposal);
103+
self.step = ConsensusStep::Prepare;
104+
}
105+
106+
/// Transition to Commit step.
107+
pub fn enter_commit(&mut self, block_hash: Hash) {
108+
self.locked_block = Some(block_hash);
109+
self.locked_round = Some(self.round);
110+
self.step = ConsensusStep::Commit;
111+
}
112+
113+
/// Finalize the current height.
114+
pub fn finalize(&mut self, block: Block) {
115+
self.last_commit = Some(block);
116+
self.step = ConsensusStep::Finalized;
117+
}
118+
119+
/// Add a prepare vote.
120+
pub fn add_prepare_vote(&mut self, vote: Vote) {
121+
if vote.vote_type == VoteType::Prepare && vote.height == self.height && vote.round == self.round {
122+
self.prepare_votes.push(vote);
123+
}
124+
}
125+
126+
/// Add a commit vote.
127+
pub fn add_commit_vote(&mut self, vote: Vote) {
128+
if vote.vote_type == VoteType::Commit && vote.height == self.height && vote.round == self.round {
129+
self.commit_votes.push(vote);
130+
}
131+
}
132+
133+
/// Check if we have 2f+1 prepare votes for a specific block.
134+
pub fn has_prepare_quorum(&self, block_hash: &Hash) -> bool {
135+
let votes_for_block: Vec<_> = self
136+
.prepare_votes
137+
.iter()
138+
.filter(|v| &v.block_hash == block_hash)
139+
.collect();
140+
141+
let addresses: Vec<_> = votes_for_block
142+
.iter()
143+
.map(|v| v.validator_address.clone())
144+
.collect();
145+
146+
self.validators.has_quorum(&addresses)
147+
}
148+
149+
/// Check if we have 2f+1 commit votes for a specific block.
150+
pub fn has_commit_quorum(&self, block_hash: &Hash) -> bool {
151+
let votes_for_block: Vec<_> = self
152+
.commit_votes
153+
.iter()
154+
.filter(|v| &v.block_hash == block_hash)
155+
.collect();
156+
157+
let addresses: Vec<_> = votes_for_block
158+
.iter()
159+
.map(|v| v.validator_address.clone())
160+
.collect();
161+
162+
self.validators.has_quorum(&addresses)
163+
}
164+
165+
/// Get the current proposer for this round.
166+
pub fn proposer(&self) -> &types::Validator {
167+
self.validators.proposer()
168+
}
169+
170+
/// Check if we are the proposer for this round.
171+
pub fn is_proposer(&self, address: &[u8]) -> bool {
172+
self.proposer().address == address
173+
}
174+
175+
/// Reset state for new height.
176+
pub fn advance_height(&mut self, new_height: Height, new_validators: ValidatorSet) {
177+
self.height = new_height;
178+
self.round = Round::default();
179+
self.step = ConsensusStep::NewHeight;
180+
self.validators = new_validators;
181+
self.proposal = None;
182+
self.locked_block = None;
183+
self.locked_round = None;
184+
self.valid_block = None;
185+
self.valid_round = None;
186+
self.prepare_votes.clear();
187+
self.commit_votes.clear();
188+
}
189+
}
190+
191+
#[cfg(test)]
192+
mod tests {
193+
use super::*;
194+
use types::Validator;
195+
196+
fn create_test_validators() -> ValidatorSet {
197+
let validators = vec![
198+
Validator::new(vec![1], vec![1; 32], 10),
199+
Validator::new(vec![2], vec![2; 32], 10),
200+
Validator::new(vec![3], vec![3; 32], 10),
201+
Validator::new(vec![4], vec![4; 32], 10),
202+
];
203+
204+
ValidatorSet::new(validators, Height::new(1).expect("valid height"))
205+
.expect("valid validator set")
206+
}
207+
208+
#[test]
209+
fn test_consensus_state_creation() {
210+
let validators = create_test_validators();
211+
let state = ConsensusState::new(Height::new(1).expect("valid height"), validators);
212+
213+
assert_eq!(state.height, Height::new(1).expect("valid height"));
214+
assert_eq!(state.round, Round::default());
215+
assert_eq!(state.step, ConsensusStep::NewHeight);
216+
}
217+
218+
#[test]
219+
fn test_step_transitions() {
220+
let validators = create_test_validators();
221+
let mut state = ConsensusState::new(Height::new(1).expect("valid height"), validators);
222+
223+
state.enter_propose();
224+
assert_eq!(state.step, ConsensusStep::Propose);
225+
226+
let block = Block::new(
227+
types::BlockHeader::new(
228+
Height::new(1).expect("valid height"),
229+
Hash::new([1; 32]),
230+
Round::new(0),
231+
chrono::Utc::now(),
232+
vec![1],
233+
Hash::new([0; 32]),
234+
Hash::new([2; 32]),
235+
Hash::new([3; 32]),
236+
),
237+
types::BlockData::new(vec![]),
238+
None,
239+
None,
240+
vec![],
241+
);
242+
243+
state.enter_prepare(block.clone());
244+
assert_eq!(state.step, ConsensusStep::Prepare);
245+
assert!(state.proposal.is_some());
246+
247+
state.enter_commit(block.hash());
248+
assert_eq!(state.step, ConsensusStep::Commit);
249+
assert!(state.locked_block.is_some());
250+
}
251+
252+
#[test]
253+
fn test_round_advancement() {
254+
let validators = create_test_validators();
255+
let mut state = ConsensusState::new(Height::new(1).expect("valid height"), validators);
256+
257+
state.enter_new_round(Round::new(1));
258+
assert_eq!(state.round, Round::new(1));
259+
assert_eq!(state.step, ConsensusStep::NewHeight);
260+
assert!(state.proposal.is_none());
261+
}
262+
263+
#[test]
264+
fn test_height_advancement() {
265+
let validators = create_test_validators();
266+
let mut state = ConsensusState::new(Height::new(1).expect("valid height"), validators.clone());
267+
268+
state.advance_height(Height::new(2).expect("valid height"), validators);
269+
assert_eq!(state.height, Height::new(2).expect("valid height"));
270+
assert_eq!(state.round, Round::default());
271+
assert_eq!(state.step, ConsensusStep::NewHeight);
272+
}
273+
274+
#[test]
275+
fn test_proposer_check() {
276+
let validators = create_test_validators();
277+
let state = ConsensusState::new(Height::new(1).expect("valid height"), validators);
278+
279+
let proposer = state.proposer();
280+
assert!(state.is_proposer(&proposer.address));
281+
assert!(!state.is_proposer(&[99]));
282+
}
20283
}

0 commit comments

Comments
 (0)