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 = 10;
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,
current_round_exp: u8,
}
impl<C: Context> RoundSuccessMeter<C> {
pub fn new(round_exp: u8, min_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,
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 + 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
}
}
}