use {
crate::{
common::{
SAFE_TO_NOTAR_MIN_NOTARIZE_AND_SKIP, SAFE_TO_NOTAR_MIN_NOTARIZE_FOR_NOTARIZE_OR_SKIP,
SAFE_TO_NOTAR_MIN_NOTARIZE_ONLY, SAFE_TO_SKIP_THRESHOLD, Stake,
},
consensus_pool::stats::ConsensusPoolStats,
event::VotorEvent,
},
agave_votor_messages::{fraction::Fraction, vote::Vote},
solana_hash::Hash,
std::{collections::BTreeMap, num::NonZeroU64},
};
#[derive(Debug, Default)]
pub(crate) struct SlotStakeCounters {
my_first_vote: Option<Vote>,
total_stake: Stake,
skip_total: Stake,
notarize_total: Stake,
notarize_entry_total: BTreeMap<Hash, Stake>,
top_notarized_stake: Stake,
safe_to_notar_sent: Vec<Hash>,
safe_to_skip_sent: bool,
}
impl SlotStakeCounters {
pub fn new(total_stake: Stake) -> Self {
Self {
total_stake,
..Default::default()
}
}
pub fn add_vote(
&mut self,
vote: &Vote,
entry_stake: Stake,
is_my_own_vote: bool,
events: &mut Vec<VotorEvent>,
stats: &mut ConsensusPoolStats,
) {
match vote {
Vote::Skip(_) => self.skip_total = entry_stake,
Vote::Notarize(vote) => {
let old_entry_stake = self
.notarize_entry_total
.insert(vote.block_id, entry_stake)
.unwrap_or(0);
self.notarize_total = self
.notarize_total
.saturating_sub(old_entry_stake)
.saturating_add(entry_stake);
self.top_notarized_stake = self.top_notarized_stake.max(entry_stake);
}
_ => return, }
if self.my_first_vote.is_none() && is_my_own_vote {
self.my_first_vote = Some(*vote);
}
if self.my_first_vote.is_none() {
return;
}
let slot = vote.slot();
for (block_id, stake) in &self.notarize_entry_total {
if !self.safe_to_notar_sent.contains(block_id) && self.is_safe_to_notar(block_id, stake)
{
events.push(VotorEvent::SafeToNotar((slot, *block_id)));
stats.event_safe_to_notarize = stats.event_safe_to_notarize.saturating_add(1);
self.safe_to_notar_sent.push(*block_id);
}
}
if !self.safe_to_skip_sent && self.is_safe_to_skip() {
events.push(VotorEvent::SafeToSkip(slot));
self.safe_to_skip_sent = true;
stats.event_safe_to_skip = stats.event_safe_to_skip.saturating_add(1);
}
}
fn is_safe_to_notar(&self, block_id: &Hash, stake: &Stake) -> bool {
if let Some(Vote::Notarize(my_vote)) = self.my_first_vote.as_ref() {
if &my_vote.block_id == block_id {
return false; }
}
trace!(
"safe_to_notar {block_id:?} skip_ratio={} notarized_ratio={}",
self.skip_total as f64 / self.total_stake as f64,
*stake as f64 / self.total_stake as f64
);
let total_stake = NonZeroU64::new(self.total_stake).unwrap();
let notarized_ratio = Fraction::new(*stake, total_stake);
let notarized_plus_skip_ratio =
Fraction::new(self.skip_total.checked_add(*stake).unwrap(), total_stake);
notarized_ratio >= SAFE_TO_NOTAR_MIN_NOTARIZE_ONLY
|| (notarized_ratio >= SAFE_TO_NOTAR_MIN_NOTARIZE_FOR_NOTARIZE_OR_SKIP
&& notarized_plus_skip_ratio >= SAFE_TO_NOTAR_MIN_NOTARIZE_AND_SKIP)
}
fn is_safe_to_skip(&self) -> bool {
if let Some(Vote::Notarize(_)) = self.my_first_vote.as_ref() {
trace!(
"safe_to_skip {} {:?} {} {} {}",
self.my_first_vote.unwrap().slot(),
self.my_first_vote.unwrap().block_id(),
self.skip_total,
self.notarize_total,
self.top_notarized_stake
);
let num_stake = self
.skip_total
.saturating_add(self.notarize_total.saturating_sub(self.top_notarized_stake));
let total_stake = NonZeroU64::new(self.total_stake).unwrap();
Fraction::new(num_stake, total_stake) >= SAFE_TO_SKIP_THRESHOLD
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use {super::*, agave_votor_messages::vote::Vote};
#[test]
fn test_safe_to_notar() {
let mut counters = SlotStakeCounters::new(100);
let mut events = vec![];
let mut stats = ConsensusPoolStats::default();
let slot = 2;
counters.add_vote(
&Vote::new_skip_vote(slot),
10,
true,
&mut events,
&mut stats,
);
assert!(events.is_empty());
assert_eq!(stats.event_safe_to_notarize, 0);
counters.add_vote(
&Vote::new_notarization_vote(slot, Hash::default()),
40,
false,
&mut events,
&mut stats,
);
assert_eq!(events.len(), 1);
assert!(
matches!(events[0], VotorEvent::SafeToNotar((s, block_id)) if s == slot && block_id == Hash::default())
);
assert_eq!(stats.event_safe_to_notarize, 1);
events.clear();
counters.add_vote(
&Vote::new_notarization_vote(slot, Hash::default()),
20,
false,
&mut events,
&mut stats,
);
assert!(events.is_empty());
assert_eq!(stats.event_safe_to_notarize, 1);
counters = SlotStakeCounters::new(100);
events.clear();
stats = ConsensusPoolStats::default();
let hash_1 = Hash::new_unique();
counters.add_vote(
&Vote::new_notarization_vote(slot, hash_1),
1,
true,
&mut events,
&mut stats,
);
assert!(events.is_empty());
assert_eq!(stats.event_safe_to_notarize, 0);
let hash_2 = Hash::new_unique();
counters.add_vote(
&Vote::new_notarization_vote(slot, hash_2),
25,
false,
&mut events,
&mut stats,
);
assert!(events.is_empty());
assert_eq!(stats.event_safe_to_notarize, 0);
counters.add_vote(
&Vote::new_skip_vote(slot),
35,
false,
&mut events,
&mut stats,
);
assert_eq!(events.len(), 1);
assert!(
matches!(events[0], VotorEvent::SafeToNotar((s, block_id)) if s == slot && block_id == hash_2)
);
assert_eq!(stats.event_safe_to_notarize, 1);
}
#[test]
fn test_safe_to_skip() {
let mut counters = SlotStakeCounters::new(100);
let mut events = vec![];
let mut stats = ConsensusPoolStats::default();
let slot = 2;
counters.add_vote(
&Vote::new_notarization_vote(slot, Hash::default()),
10,
true,
&mut events,
&mut stats,
);
assert!(events.is_empty());
assert_eq!(stats.event_safe_to_skip, 0);
counters.add_vote(
&Vote::new_skip_vote(slot),
40,
false,
&mut events,
&mut stats,
);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], VotorEvent::SafeToSkip(s) if s == slot));
assert_eq!(stats.event_safe_to_skip, 1);
events.clear();
counters.add_vote(
&Vote::new_skip_vote(slot),
20,
false,
&mut events,
&mut stats,
);
assert!(events.is_empty());
assert_eq!(stats.event_safe_to_skip, 1);
counters = SlotStakeCounters::new(100);
events.clear();
stats = ConsensusPoolStats::default();
let hash_1 = Hash::new_unique();
counters.add_vote(
&Vote::new_notarization_vote(slot, hash_1),
10,
true,
&mut events,
&mut stats,
);
let hash_2 = Hash::new_unique();
counters.add_vote(
&Vote::new_notarization_vote(slot, hash_2),
20,
false,
&mut events,
&mut stats,
);
counters.add_vote(
&Vote::new_skip_vote(slot),
30,
false,
&mut events,
&mut stats,
);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], VotorEvent::SafeToSkip(s) if s == slot));
assert_eq!(stats.event_safe_to_skip, 1);
events.clear();
counters.add_vote(
&Vote::new_notarization_vote(slot, hash_1),
10,
false,
&mut events,
&mut stats,
);
assert!(events.is_empty());
assert_eq!(stats.event_safe_to_skip, 1);
}
}