Skip to content

Commit

Permalink
feat(driver): Allow the driver to raise errors in some occasions (#62)
Browse files Browse the repository at this point in the history
* feat(driver): Allow the driver to raise errors in some occasions

* Allow test env to not supply a value

* Exclude some standard instances from code coverage

* Add unit tests for error cases
  • Loading branch information
romac authored Nov 14, 2023
1 parent b2d63ed commit 01cfa7e
Show file tree
Hide file tree
Showing 10 changed files with 326 additions and 74 deletions.
4 changes: 2 additions & 2 deletions Code/common/src/validator_set.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use core::fmt::Debug;
use core::fmt::{Debug, Display};

use crate::{Context, PublicKey};

Expand All @@ -12,7 +12,7 @@ pub type VotingPower = u64;
/// TODO: Keep this trait or just add the bounds to Consensus::Address?
pub trait Address
where
Self: Clone + Debug + Eq + Ord,
Self: Clone + Debug + Display + Eq + Ord,
{
}

Expand Down
74 changes: 47 additions & 27 deletions Code/driver/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use malachite_vote::Threshold;
use crate::env::Env as DriverEnv;
use crate::event::Event;
use crate::message::Message;
use crate::Error;
use crate::ProposerSelector;

/// Driver for the state machine of the Malachite consensus engine at a given height.
Expand Down Expand Up @@ -68,67 +69,78 @@ where
}
}

async fn get_value(&self) -> Ctx::Value {
self.env.get_value().await
async fn get_value(&self, round: Round) -> Option<Ctx::Value> {
self.env.get_value(self.height.clone(), round).await
}

async fn validate_proposal(&self, proposal: &Ctx::Proposal) -> bool {
self.env.validate_proposal(proposal).await
}

pub async fn execute(&mut self, msg: Event<Ctx>) -> Option<Message<Ctx>> {
let round_msg = match self.apply(msg).await {
pub async fn execute(&mut self, msg: Event<Ctx>) -> Result<Option<Message<Ctx>>, Error<Ctx>> {
let round_msg = match self.apply(msg).await? {
Some(msg) => msg,
None => return None,
None => return Ok(None),
};

match round_msg {
let msg = match round_msg {
RoundMessage::NewRound(round) => {
// XXX: Check if there is an existing state?
assert!(self.round < round);
Some(Message::NewRound(round))
Message::NewRound(round)
}

RoundMessage::Proposal(proposal) => {
// sign the proposal
Some(Message::Propose(proposal))
Message::Propose(proposal)
}

RoundMessage::Vote(vote) => {
let signed_vote = self.ctx.sign_vote(vote);
Some(Message::Vote(signed_vote))
Message::Vote(signed_vote)
}

RoundMessage::ScheduleTimeout(timeout) => Some(Message::ScheduleTimeout(timeout)),
RoundMessage::ScheduleTimeout(timeout) => Message::ScheduleTimeout(timeout),

RoundMessage::Decision(value) => {
// TODO: update the state
Some(Message::Decide(value.round, value.value))
Message::Decide(value.round, value.value)
}
}
};

Ok(Some(msg))
}

async fn apply(&mut self, msg: Event<Ctx>) -> Option<RoundMessage<Ctx>> {
async fn apply(&mut self, msg: Event<Ctx>) -> Result<Option<RoundMessage<Ctx>>, Error<Ctx>> {
match msg {
Event::NewRound(round) => self.apply_new_round(round).await,
Event::Proposal(proposal) => self.apply_proposal(proposal).await,
Event::Proposal(proposal) => Ok(self.apply_proposal(proposal).await),
Event::Vote(signed_vote) => self.apply_vote(signed_vote),
Event::TimeoutElapsed(timeout) => self.apply_timeout(timeout),
Event::TimeoutElapsed(timeout) => Ok(self.apply_timeout(timeout)),
}
}

async fn apply_new_round(&mut self, round: Round) -> Option<RoundMessage<Ctx>> {
async fn apply_new_round(
&mut self,
round: Round,
) -> Result<Option<RoundMessage<Ctx>>, Error<Ctx>> {
let proposer_address = self
.proposer_selector
.select_proposer(round, &self.validator_set);

let proposer = self
.validator_set
.get_by_address(&proposer_address)
.expect("proposer not found"); // FIXME: expect
.ok_or_else(|| Error::ProposerNotFound(proposer_address.clone()))?;

let event = if proposer.address() == &self.address {
let value = self.get_value().await;
// We are the proposer
// TODO: Schedule propose timeout

let Some(value) = self.get_value(round).await else {
return Err(Error::NoValueToPropose);
};

RoundEvent::NewRoundProposer(value)
} else {
RoundEvent::NewRound
Expand All @@ -139,7 +151,7 @@ where
.insert(round, RoundState::default().new_round(round));
self.round = round;

self.apply_event(round, event)
Ok(self.apply_event(round, event))
}

async fn apply_proposal(&mut self, proposal: Ctx::Proposal) -> Option<RoundMessage<Ctx>> {
Expand Down Expand Up @@ -201,25 +213,33 @@ where
}
}

fn apply_vote(&mut self, signed_vote: SignedVote<Ctx>) -> Option<RoundMessage<Ctx>> {
// TODO: How to handle missing validator?
fn apply_vote(
&mut self,
signed_vote: SignedVote<Ctx>,
) -> Result<Option<RoundMessage<Ctx>>, Error<Ctx>> {
let validator = self
.validator_set
.get_by_address(signed_vote.validator_address())?;
.get_by_address(signed_vote.validator_address())
.ok_or_else(|| Error::ValidatorNotFound(signed_vote.validator_address().clone()))?;

if !self
.ctx
.verify_signed_vote(&signed_vote, validator.public_key())
{
// TODO: How to handle invalid votes?
return None;
return Err(Error::InvalidVoteSignature(
signed_vote.clone(),
validator.clone(),
));
}

let round = signed_vote.vote.round();

let vote_msg = self
let Some(vote_msg) = self
.votes
.apply_vote(signed_vote.vote, validator.voting_power())?;
.apply_vote(signed_vote.vote, validator.voting_power())
else {
return Ok(None);
};

let round_event = match vote_msg {
VoteMessage::PolkaAny => RoundEvent::PolkaAny,
Expand All @@ -230,7 +250,7 @@ where
VoteMessage::SkipRound(r) => RoundEvent::SkipRound(r),
};

self.apply_event(round, round_event)
Ok(self.apply_event(round, round_event))
}

fn apply_timeout(&mut self, timeout: Timeout) -> Option<RoundMessage<Ctx>> {
Expand Down
9 changes: 6 additions & 3 deletions Code/driver/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use alloc::boxed::Box;

use async_trait::async_trait;

use malachite_common::Context;
use malachite_common::{Context, Round};

/// Environment for use by the [`Driver`](crate::Driver) to ask
/// for a value to propose and validate proposals.
Expand All @@ -11,8 +11,11 @@ pub trait Env<Ctx>
where
Ctx: Context,
{
/// Get the value to propose.
async fn get_value(&self) -> Ctx::Value;
/// Get the value to propose for the given height and round.
///
/// If `None` is returned, the driver will understand this
/// as an error and will not propose a value.
async fn get_value(&self, height: Ctx::Height, round: Round) -> Option<Ctx::Value>;

/// Validate a proposal.
async fn validate_proposal(&self, proposal: &Ctx::Proposal) -> bool;
Expand Down
59 changes: 59 additions & 0 deletions Code/driver/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use core::fmt;

use malachite_common::{Context, SignedVote, Validator};

#[derive(Clone, Debug)]
pub enum Error<Ctx>
where
Ctx: Context,
{
/// No value to propose
NoValueToPropose,

/// Proposer not found
ProposerNotFound(Ctx::Address),

/// Validator not found in validator set
ValidatorNotFound(Ctx::Address),

/// Invalid vote signature
InvalidVoteSignature(SignedVote<Ctx>, Ctx::Validator),
}

impl<Ctx> fmt::Display for Error<Ctx>
where
Ctx: Context,
{
#[cfg_attr(coverage_nightly, coverage(off))]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::NoValueToPropose => write!(f, "No value to propose"),
Error::ProposerNotFound(addr) => write!(f, "Proposer not found: {addr}"),
Error::ValidatorNotFound(addr) => write!(f, "Validator not found: {addr}"),
Error::InvalidVoteSignature(vote, validator) => write!(
f,
"Invalid vote signature by {} on vote {vote:?}",
validator.address()
),
}
}
}

impl<Ctx> PartialEq for Error<Ctx>
where
Ctx: Context,
{
#[cfg_attr(coverage_nightly, coverage(off))]
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Error::NoValueToPropose, Error::NoValueToPropose) => true,
(Error::ProposerNotFound(addr1), Error::ProposerNotFound(addr2)) => addr1 == addr2,
(Error::ValidatorNotFound(addr1), Error::ValidatorNotFound(addr2)) => addr1 == addr2,
(
Error::InvalidVoteSignature(vote1, validator1),
Error::InvalidVoteSignature(vote2, validator2),
) => vote1 == vote2 && validator1 == validator2,
_ => false,
}
}
}
2 changes: 2 additions & 0 deletions Code/driver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ extern crate alloc;

mod driver;
mod env;
mod error;
mod event;
mod message;
mod proposer;

pub use driver::Driver;
pub use env::Env;
pub use error::Error;
pub use event::Event;
pub use message::Message;
pub use proposer::ProposerSelector;
Expand Down
21 changes: 14 additions & 7 deletions Code/test/src/env.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
use async_trait::async_trait;

use malachite_common::Round;
use malachite_driver::Env;

use crate::{Proposal, TestContext, Value};
use crate::{Height, Proposal, TestContext, Value};

pub struct TestEnv {
pub value: Value,
pub is_valid: fn(&Proposal) -> bool,
get_value: Box<dyn Fn(Height, Round) -> Option<Value> + Send + Sync>,
is_valid: Box<dyn Fn(&Proposal) -> bool + Send + Sync>,
}

impl TestEnv {
pub fn new(value: Value, is_valid: fn(&Proposal) -> bool) -> Self {
Self { value, is_valid }
pub fn new(
get_value: impl Fn(Height, Round) -> Option<Value> + Send + Sync + 'static,
is_valid: impl Fn(&Proposal) -> bool + Send + Sync + 'static,
) -> Self {
Self {
get_value: Box::new(get_value),
is_valid: Box::new(is_valid),
}
}
}

#[async_trait]
impl Env<TestContext> for TestEnv {
async fn get_value(&self) -> Value {
self.value.clone()
async fn get_value(&self, height: Height, round: Round) -> Option<Value> {
(self.get_value)(height, round)
}

async fn validate_proposal(&self, proposal: &Proposal) -> bool {
Expand Down
12 changes: 12 additions & 0 deletions Code/test/src/validator_set.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use core::fmt;

use malachite_common::VotingPower;

use crate::{signing::PublicKey, TestContext};
Expand All @@ -22,6 +24,16 @@ impl Address {
}
}

impl fmt::Display for Address {
#[cfg_attr(coverage_nightly, coverage(off))]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in self.0.iter() {
write!(f, "{:02x}", byte)?;
}
Ok(())
}
}

impl malachite_common::Address for Address {}

/// A validator is a public key and voting power
Expand Down
2 changes: 1 addition & 1 deletion Code/test/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ impl From<u64> for ValueId {
}

/// The value to decide on
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Value(u64);

impl Value {
Expand Down
Loading

0 comments on commit 01cfa7e

Please sign in to comment.