use std::{cmp::max, collections::VecDeque, mem};
use datasize::DataSize;
use tracing::trace;
use crate::{
components::consensus::{
highway_core::{finality_detector::FinalityDetector, round_id, State, Weight},
traits::Context,
},
types::Timestamp,
};
const NUM_ROUNDS_TO_CONSIDER: usize = 40;
const MAX_FAILED_ROUNDS: usize = 20;
const ACCELERATION_PARAMETER: u64 = 40;
const MAX_FAILURES_FOR_ACCELERATION: usize = 3;
const THRESHOLD: u64 = 1;
#[derive(DataSize, Debug)]
pub(crate) struct RoundSuccessMeter<C>
where
C: Context,
{
rounds: VecDeque<bool>,
current_round_id: u64,
proposals: Vec<C::Hash>,
min_round_exp: u8,
max_round_exp: u8,
current_round_exp: u8,
}
impl<C: Context> RoundSuccessMeter<C> {
pub fn new(round_exp: u8, min_round_exp: u8, max_round_exp: u8, timestamp: Timestamp) -> Self {
let current_round_id = round_id(timestamp, round_exp).millis();
Self {
rounds: VecDeque::with_capacity(NUM_ROUNDS_TO_CONSIDER),
current_round_id,
proposals: Vec::new(),
min_round_exp,
max_round_exp,
current_round_exp: round_exp,
}
}
fn change_exponent(&mut self, new_exp: u8, timestamp: Timestamp) {
self.rounds = VecDeque::with_capacity(NUM_ROUNDS_TO_CONSIDER);
self.current_round_exp = new_exp;
self.current_round_id = round_id(timestamp, new_exp).millis();
self.proposals = Vec::new();
}
fn check_proposals_success(&self, state: &State<C>, proposal_h: &C::Hash) -> bool {
let total_w = state.total_weight();
let finality_detector =
FinalityDetector::<C>::new(max(total_w / 100 * THRESHOLD, Weight(1)));
finality_detector.find_summit(1, proposal_h, state) == 1
}
pub fn new_proposal(&mut self, proposal_h: C::Hash, timestamp: Timestamp) {
if round_id(timestamp, self.current_round_exp).millis() == self.current_round_id {
trace!(
%self.current_round_id,
timestamp = timestamp.millis(),
"adding a proposal"
);
self.proposals.push(proposal_h);
} else {
trace!(
%self.current_round_id,
timestamp = timestamp.millis(),
%self.current_round_exp,
"trying to add proposal for a different round!"
);
}
}
pub fn calculate_new_exponent(&mut self, state: &State<C>) -> u8 {
let now = Timestamp::now();
if round_id(now, self.current_round_exp).millis() <= self.current_round_id {
return self.new_exponent();
}
trace!(%self.current_round_id, "calculating exponent");
let current_round_index = self.current_round_id >> self.current_round_exp;
let new_round_index = now.millis() >> self.current_round_exp;
if mem::take(&mut self.proposals)
.into_iter()
.any(|proposal| self.check_proposals_success(state, &proposal))
{
trace!("round succeeded");
self.rounds.push_front(true);
} else {
trace!("round failed");
self.rounds.push_front(false);
}
for _ in 0..(new_round_index - current_round_index - 1) {
trace!("round failed");
self.rounds.push_front(false);
}
self.current_round_id = new_round_index << self.current_round_exp;
self.clean_old_rounds();
trace!(
%self.current_round_exp,
"{} failures among the last {} rounds.",
self.count_failures(),
self.rounds.len()
);
let new_exp = self.new_exponent();
trace!(%new_exp, "new exponent calculated");
if new_exp != self.current_round_exp {
self.change_exponent(new_exp, now);
}
new_exp
}
fn clean_old_rounds(&mut self) {
while self.rounds.len() > NUM_ROUNDS_TO_CONSIDER {
self.rounds.pop_back();
}
}
fn count_failures(&self) -> usize {
self.rounds.iter().filter(|&success| !success).count()
}
fn new_exponent(&self) -> u8 {
let current_round_index = self.current_round_id >> self.current_round_exp;
let num_failures = self.count_failures();
if num_failures > MAX_FAILED_ROUNDS && self.current_round_exp < self.max_round_exp {
self.current_round_exp + 1
} else if current_round_index % ACCELERATION_PARAMETER == 0
&& self.current_round_exp > self.min_round_exp
&& self.rounds.len() == NUM_ROUNDS_TO_CONSIDER
&& num_failures < MAX_FAILURES_FOR_ACCELERATION
{
self.current_round_exp - 1
} else {
self.current_round_exp
}
}
}
#[cfg(test)]
mod tests {
use crate::components::consensus::{
cl_context::ClContext,
protocols::highway::round_success_meter::{
ACCELERATION_PARAMETER, MAX_FAILED_ROUNDS, NUM_ROUNDS_TO_CONSIDER,
},
};
const TEST_ROUND_EXP: u8 = 13;
const TEST_MIN_ROUND_EXP: u8 = 8;
const TEST_MAX_ROUND_EXP: u8 = 19;
#[test]
fn new_exponent_steady() {
let round_success_meter: super::RoundSuccessMeter<ClContext> =
super::RoundSuccessMeter::new(
TEST_ROUND_EXP,
TEST_MIN_ROUND_EXP,
TEST_MAX_ROUND_EXP,
crate::types::Timestamp::now(),
);
assert_eq!(round_success_meter.new_exponent(), TEST_ROUND_EXP);
}
#[test]
fn new_exponent_slow_down() {
let mut round_success_meter: super::RoundSuccessMeter<ClContext> =
super::RoundSuccessMeter::new(
TEST_ROUND_EXP,
TEST_MIN_ROUND_EXP,
TEST_MAX_ROUND_EXP,
crate::types::Timestamp::now(),
);
round_success_meter.rounds = vec![false; MAX_FAILED_ROUNDS + 1].into();
assert_eq!(round_success_meter.new_exponent(), TEST_ROUND_EXP + 1);
}
#[test]
fn new_exponent_can_not_slow_down_because_max_round_exp() {
let mut round_success_meter: super::RoundSuccessMeter<ClContext> =
super::RoundSuccessMeter::new(
TEST_MAX_ROUND_EXP,
TEST_MIN_ROUND_EXP,
TEST_MAX_ROUND_EXP,
crate::types::Timestamp::now(),
);
round_success_meter.rounds = vec![false; MAX_FAILED_ROUNDS + 1].into();
assert_eq!(round_success_meter.new_exponent(), TEST_MAX_ROUND_EXP);
}
#[test]
fn new_exponent_speed_up() {
let mut round_success_meter: super::RoundSuccessMeter<ClContext> =
super::RoundSuccessMeter::new(
TEST_ROUND_EXP,
TEST_MIN_ROUND_EXP,
TEST_MAX_ROUND_EXP,
crate::types::Timestamp::now(),
);
round_success_meter.rounds = vec![true; NUM_ROUNDS_TO_CONSIDER].into();
loop {
let current_round_index =
round_success_meter.current_round_id >> round_success_meter.current_round_exp;
if current_round_index % ACCELERATION_PARAMETER == 0 {
break;
};
round_success_meter.current_round_id += 1;
}
assert_eq!(round_success_meter.new_exponent(), TEST_ROUND_EXP - 1);
}
#[test]
fn new_exponent_can_not_speed_up_because_min_round_exp() {
let mut round_success_meter: super::RoundSuccessMeter<ClContext> =
super::RoundSuccessMeter::new(
TEST_MIN_ROUND_EXP,
TEST_MIN_ROUND_EXP,
TEST_MAX_ROUND_EXP,
crate::types::Timestamp::now(),
);
round_success_meter.rounds = vec![true; NUM_ROUNDS_TO_CONSIDER].into();
loop {
let current_round_index =
round_success_meter.current_round_id >> round_success_meter.current_round_exp;
if current_round_index % ACCELERATION_PARAMETER == 0 {
break;
};
round_success_meter.current_round_id += 1;
}
assert_eq!(round_success_meter.new_exponent(), TEST_MIN_ROUND_EXP);
}
}