use std::cmp::Ordering;
use std::collections::hash_map::Entry;
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::iter;
use std::ops::Deref;
use std::sync::Arc;
use machine::{machine, transitions};
use num::{rational::Ratio, Zero};
use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive};
use parking_lot::RwLock;
use thiserror::Error;
use tracing::{debug, warn};
use massa_db_exports::{
DBBatch, ShareableMassaDBController, MIP_STORE_PREFIX, MIP_STORE_STATS_PREFIX, STATE_CF,
VERSIONING_CF,
};
use massa_models::config::MIP_STORE_STATS_BLOCK_CONSIDERED;
#[allow(unused_imports)]
use massa_models::config::VERSIONING_ACTIVATION_DELAY_MIN;
use massa_models::config::VERSIONING_THRESHOLD_TRANSITION_ACCEPTED;
use massa_models::error::ModelsError;
use massa_models::slot::Slot;
use massa_models::timeslots::get_block_slot_timestamp;
use massa_serialization::{DeserializeError, Deserializer, SerializeError, Serializer};
use massa_time::MassaTime;
use variant_count::VariantCount;
use crate::versioning_ser_der::{
MipInfoDeserializer, MipInfoSerializer, MipStateDeserializer, MipStateSerializer,
MipStoreStatsDeserializer, MipStoreStatsSerializer,
};
#[allow(missing_docs)]
#[derive(
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, FromPrimitive, IntoPrimitive, VariantCount,
)]
#[repr(u32)]
pub enum MipComponent {
Address,
KeyPair,
Block,
VM,
FinalStateHashKind,
Execution,
FinalState,
#[doc(hidden)]
#[num_enum(default)]
__Nonexhaustive,
}
#[derive(Clone, Debug)]
pub struct MipInfo {
pub name: String,
pub version: u32,
pub components: BTreeMap<MipComponent, u32>,
pub start: MassaTime,
pub timeout: MassaTime,
pub activation_delay: MassaTime,
}
impl Ord for MipInfo {
fn cmp(&self, other: &Self) -> Ordering {
(
self.start,
self.timeout,
self.activation_delay,
&self.name,
&self.version,
&self.components,
)
.cmp(&(
other.start,
other.timeout,
other.activation_delay,
&other.name,
&other.version,
&other.components,
))
}
}
impl PartialOrd for MipInfo {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for MipInfo {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.version == other.version
&& self.components == other.components
&& self.start == other.start
&& self.timeout == other.timeout
&& self.activation_delay == other.activation_delay
}
}
impl Eq for MipInfo {}
machine!(
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum ComponentState {
Defined,
Started { pub(crate) vote_ratio: Ratio<u64> },
LockedIn { pub(crate) at: MassaTime },
Active { pub(crate) at: MassaTime },
Failed,
}
);
impl Default for ComponentState {
fn default() -> Self {
Self::Defined(Defined {})
}
}
impl ComponentState {
fn is_final(&self) -> bool {
matches!(
self,
ComponentState::Active(..) | ComponentState::Failed(..) | ComponentState::Error
)
}
}
#[allow(missing_docs)]
#[derive(
IntoPrimitive, Debug, Clone, Eq, PartialEq, TryFromPrimitive, PartialOrd, Ord, VariantCount,
)]
#[repr(u32)]
pub enum ComponentStateTypeId {
Error = 0,
Defined = 1,
Started = 2,
LockedIn = 3,
Active = 4,
Failed = 5,
}
impl From<&ComponentState> for ComponentStateTypeId {
fn from(value: &ComponentState) -> Self {
match value {
ComponentState::Error => ComponentStateTypeId::Error,
ComponentState::Defined(_) => ComponentStateTypeId::Defined,
ComponentState::Started(_) => ComponentStateTypeId::Started,
ComponentState::LockedIn(_) => ComponentStateTypeId::LockedIn,
ComponentState::Active(_) => ComponentStateTypeId::Active,
ComponentState::Failed(_) => ComponentStateTypeId::Failed,
}
}
}
#[derive(Clone, Debug)]
pub struct Advance {
pub start_timestamp: MassaTime,
pub timeout: MassaTime,
pub activation_delay: MassaTime,
pub threshold: Ratio<u64>,
pub now: MassaTime,
}
impl PartialEq for Advance {
fn eq(&self, other: &Self) -> bool {
self.start_timestamp == other.start_timestamp
&& self.timeout == other.timeout
&& self.threshold == other.threshold
&& self.now == other.now
&& self.activation_delay == other.activation_delay
}
}
impl Eq for Advance {}
#[derive(Clone, Debug)]
pub struct AdvanceLW {
pub threshold: Ratio<u64>,
pub now: MassaTime,
}
impl From<&Advance> for AdvanceLW {
fn from(value: &Advance) -> Self {
Self {
threshold: value.threshold,
now: value.now,
}
}
}
impl Ord for AdvanceLW {
fn cmp(&self, other: &Self) -> Ordering {
(self.now, self.threshold).cmp(&(other.now, other.threshold))
}
}
impl PartialOrd for AdvanceLW {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for AdvanceLW {
fn eq(&self, other: &Self) -> bool {
self.threshold == other.threshold && self.now == other.now
}
}
impl Eq for AdvanceLW {}
transitions!(ComponentState,
[
(Defined, Advance) => [Defined, Started, Failed],
(Started, Advance) => [Started, LockedIn, Failed],
(LockedIn, Advance) => [LockedIn, Active],
(Active, Advance) => Active,
(Failed, Advance) => Failed
]
);
impl Defined {
pub fn on_advance(self, input: Advance) -> ComponentState {
match input.now {
n if n >= input.timeout => ComponentState::failed(),
n if n >= input.start_timestamp => ComponentState::started(Ratio::zero()),
_ => ComponentState::Defined(Defined {}),
}
}
}
impl Started {
pub fn on_advance(self, input: Advance) -> ComponentState {
if input.now >= input.timeout {
return ComponentState::failed();
}
if input.threshold >= VERSIONING_THRESHOLD_TRANSITION_ACCEPTED {
debug!("(VERSIONING LOG) transition accepted, locking in");
ComponentState::locked_in(input.now)
} else {
ComponentState::started(input.threshold)
}
}
}
impl LockedIn {
pub fn on_advance(self, input: Advance) -> ComponentState {
if input.now > self.at.saturating_add(input.activation_delay) {
debug!("(VERSIONING LOG) locked version has become active");
ComponentState::active(input.now)
} else {
ComponentState::locked_in(self.at)
}
}
}
impl Active {
pub fn on_advance(self, _input: Advance) -> Active {
Active { at: self.at }
}
}
impl Failed {
pub fn on_advance(self, _input: Advance) -> Failed {
Failed {}
}
}
#[derive(Error, Debug, PartialEq)]
pub enum IsConsistentError {
#[error("MipState history is empty")]
EmptyHistory,
#[error("MipState is at state Error")]
AtError,
#[error("History must start at state 'Defined' and not {0:?}")]
InvalidHistory(ComponentStateTypeId),
#[error("Non consistent state: {0:?} versus rebuilt state: {1:?}")]
NonConsistent(ComponentState, ComponentState),
#[error("Invalid data in MIP info, start >= timeout")]
Invalid,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MipState {
pub(crate) state: ComponentState,
pub(crate) history: BTreeMap<AdvanceLW, ComponentStateTypeId>,
}
impl MipState {
pub fn new(defined: MassaTime) -> Self {
let state: ComponentState = Default::default(); let state_id = ComponentStateTypeId::from(&state);
let advance = AdvanceLW {
threshold: Default::default(),
now: defined,
};
let history = BTreeMap::from([(advance, state_id)]);
Self { state, history }
}
pub fn reset_from(&self) -> Option<Self> {
match self.history.first_key_value() {
Some((advance, state_id)) if *state_id == ComponentStateTypeId::Defined => {
Some(MipState::new(advance.now))
}
_ => None,
}
}
pub fn on_advance(&mut self, input: &Advance) {
let is_forward = self
.history
.last_key_value()
.map(|(adv, _)| adv.now < input.now)
.unwrap_or(false);
if is_forward {
let state = self.state.on_advance(input.clone());
if state != self.state {
if !(matches!(state, ComponentState::Started(Started { .. }))
&& matches!(self.state, ComponentState::Started(Started { .. })))
{
self.history
.insert(input.into(), ComponentStateTypeId::from(&state));
}
self.state = state;
}
}
}
pub fn is_consistent_with(&self, mip_info: &MipInfo) -> Result<(), IsConsistentError> {
if matches!(&self.state, &ComponentState::Error) {
return Err(IsConsistentError::AtError);
}
if mip_info.start >= mip_info.timeout {
return Err(IsConsistentError::Invalid);
}
if self.history.is_empty() {
return Err(IsConsistentError::EmptyHistory);
}
let (initial_ts, initial_state_id) = self.history.first_key_value().unwrap();
if *initial_state_id != ComponentStateTypeId::Defined {
return Err(IsConsistentError::InvalidHistory(initial_state_id.clone()));
}
if mip_info.start < initial_ts.now || mip_info.timeout < initial_ts.now {
return Err(IsConsistentError::InvalidHistory(initial_state_id.clone()));
}
let mut vsh = MipState::new(initial_ts.now);
let mut advance_msg = Advance {
start_timestamp: mip_info.start,
timeout: mip_info.timeout,
threshold: Ratio::zero(),
now: initial_ts.now,
activation_delay: mip_info.activation_delay,
};
for (adv, _state) in self.history.iter().skip(1) {
advance_msg.now = adv.now;
advance_msg.threshold = adv.threshold;
vsh.on_advance(&advance_msg);
}
if let (
ComponentState::Started(Started {
vote_ratio: threshold,
}),
ComponentState::Started(Started {
vote_ratio: threshold_2,
}),
) = (vsh.state, self.state)
{
if threshold_2 != threshold {
advance_msg.threshold = threshold_2;
advance_msg.now = advance_msg.now.saturating_add(MassaTime::from_millis(1));
vsh.on_advance(&advance_msg);
}
}
if vsh == *self {
Ok(())
} else {
Err(IsConsistentError::NonConsistent(self.state, vsh.state))
}
}
pub fn state_at(
&self,
ts: MassaTime,
start: MassaTime,
timeout: MassaTime,
activation_delay: MassaTime,
) -> Result<ComponentStateTypeId, StateAtError> {
if self.history.is_empty() {
return Err(StateAtError::EmptyHistory);
}
let first = self.history.first_key_value().unwrap(); if ts < first.0.now {
return Err(StateAtError::BeforeInitialState(first.1.clone(), ts));
}
let mut lower_bound = None;
let mut higher_bound = None;
let mut is_after_last = false;
let last = self.history.last_key_value().unwrap(); if ts > last.0.now {
lower_bound = Some(last);
is_after_last = true;
}
if !is_after_last {
for (adv, state_id) in self.history.iter() {
if adv.now <= ts {
lower_bound = Some((adv, state_id));
}
if adv.now >= ts && higher_bound.is_none() {
higher_bound = Some((adv, state_id));
break;
}
}
}
match (lower_bound, higher_bound) {
(Some((_adv_1, st_id_1)), Some((_adv_2, _st_id_2))) => {
Ok(st_id_1.clone())
}
(Some((adv, st_id)), None) => {
let threshold_for_transition = VERSIONING_THRESHOLD_TRANSITION_ACCEPTED;
if *st_id == ComponentStateTypeId::Started
&& adv.threshold < threshold_for_transition
&& ts < timeout
{
Err(StateAtError::Unpredictable)
} else {
let msg = Advance {
start_timestamp: start,
timeout,
threshold: adv.threshold,
now: ts,
activation_delay,
};
let state = self.state.on_advance(msg);
Ok(ComponentStateTypeId::from(&state))
}
}
_ => {
Err(StateAtError::EmptyHistory)
}
}
}
pub fn activation_at(&self, mip_info: &MipInfo) -> Option<MassaTime> {
match self.state {
ComponentState::LockedIn(LockedIn { at }) => {
Some(at.saturating_add(mip_info.activation_delay))
}
_ => None,
}
}
pub fn is_final(&self) -> bool {
self.state.is_final()
}
}
#[allow(missing_docs)]
#[derive(Error, Debug, PartialEq)]
pub enum StateAtError {
#[error("Initial state ({0:?}) only defined after timestamp: {1}")]
BeforeInitialState(ComponentStateTypeId, MassaTime),
#[error("Empty history, should never happen")]
EmptyHistory,
#[error("Cannot predict value: threshold not reached yet")]
Unpredictable,
}
#[derive(Debug, Clone)]
pub struct MipStore(pub Arc<RwLock<MipStoreRaw>>);
impl MipStore {
pub fn get_network_version_current(&self) -> u32 {
let lock = self.0.read();
let store = lock.deref();
store
.store
.iter()
.rev()
.find_map(|(k, v)| (matches!(v.state, ComponentState::Active(_))).then_some(k.version))
.unwrap_or(0)
}
pub fn get_network_version_active_at(&self, ts: MassaTime) -> u32 {
let lock = self.0.read();
let store = lock.deref();
store
.store
.iter()
.rev()
.find_map(|(k, v)| match v.state {
ComponentState::Active(Active { at }) if at <= ts => Some(k.version),
_ => None,
})
.unwrap_or(0)
}
pub fn get_network_version_to_announce(&self) -> Option<u32> {
let lock = self.0.read();
let store = lock.deref();
store.store.iter().rev().find_map(|(k, v)| {
matches!(
&v.state,
&ComponentState::Started(_) | &ComponentState::LockedIn(_)
)
.then_some(k.version)
})
}
pub fn update_network_version_stats(
&mut self,
slot_timestamp: MassaTime,
network_versions: Option<(u32, Option<u32>)>,
) {
let mut lock = self.0.write();
lock.update_network_version_stats(slot_timestamp, network_versions);
}
#[allow(clippy::result_large_err)]
pub fn update_with(
&mut self,
mip_store: &MipStore,
) -> Result<(Vec<MipInfo>, BTreeMap<MipInfo, MipState>), UpdateWithError> {
let mut lock = self.0.write();
let lock_other = mip_store.0.read();
lock.update_with(lock_other.deref())
}
pub fn get_latest_component_version_at(&self, component: &MipComponent, ts: MassaTime) -> u32 {
let guard = self.0.read();
guard.get_latest_component_version_at(component, ts)
}
pub(crate) fn get_all_active_component_versions(&self, component: &MipComponent) -> Vec<u32> {
let guard = self.0.read();
guard.get_all_active_component_versions(component)
}
pub(crate) fn get_all_component_versions(
&self,
component: &MipComponent,
) -> BTreeMap<u32, ComponentStateTypeId> {
let guard = self.0.read();
guard.get_all_component_versions(component)
}
pub fn get_mip_status(&self) -> BTreeMap<MipInfo, ComponentStateTypeId> {
let guard = self.0.read();
guard
.store
.iter()
.map(|(mip_info, mip_state)| {
(
mip_info.clone(),
ComponentStateTypeId::from(&mip_state.state),
)
})
.collect()
}
pub fn is_consistent_with_shutdown_period(
&self,
shutdown_start: Slot,
shutdown_end: Slot,
thread_count: u8,
t0: MassaTime,
genesis_timestamp: MassaTime,
) -> Result<(), IsConsistentWithShutdownPeriodError> {
let guard = self.0.read();
guard.is_consistent_with_shutdown_period(
shutdown_start,
shutdown_end,
thread_count,
t0,
genesis_timestamp,
)
}
#[allow(dead_code)]
pub fn update_for_network_shutdown(
&mut self,
shutdown_start: Slot,
shutdown_end: Slot,
thread_count: u8,
t0: MassaTime,
genesis_timestamp: MassaTime,
) -> Result<(), ModelsError> {
let mut guard = self.0.write();
guard.update_for_network_shutdown(
shutdown_start,
shutdown_end,
thread_count,
t0,
genesis_timestamp,
)
}
pub fn is_key_value_valid(&self, serialized_key: &[u8], serialized_value: &[u8]) -> bool {
let guard = self.0.read();
guard.is_key_value_valid(serialized_key, serialized_value)
}
pub fn update_batches(
&self,
db_batch: &mut DBBatch,
db_versioning_batch: &mut DBBatch,
between: Option<(&MassaTime, &MassaTime)>,
) -> Result<(), SerializeError> {
let guard = self.0.read();
guard.update_batches(db_batch, db_versioning_batch, between)
}
pub fn extend_from_db(
&mut self,
db: ShareableMassaDBController,
) -> Result<(Vec<MipInfo>, BTreeMap<MipInfo, MipState>), ExtendFromDbError> {
let mut guard = self.0.write();
guard.extend_from_db(db)
}
pub fn reset_db(&self, db: ShareableMassaDBController) {
{
let mut guard = db.write();
guard.delete_prefix(MIP_STORE_PREFIX, STATE_CF, None);
guard.delete_prefix(MIP_STORE_PREFIX, VERSIONING_CF, None);
guard.delete_prefix(MIP_STORE_STATS_PREFIX, VERSIONING_CF, None);
}
}
pub fn try_from_db(
db: ShareableMassaDBController,
cfg: MipStatsConfig,
) -> Result<Self, ExtendFromDbError> {
MipStoreRaw::try_from_db(db, cfg).map(|store_raw| Self(Arc::new(RwLock::new(store_raw))))
}
}
impl<const N: usize> TryFrom<([(MipInfo, MipState); N], MipStatsConfig)> for MipStore {
type Error = UpdateWithError;
fn try_from(
(value, cfg): ([(MipInfo, MipState); N], MipStatsConfig),
) -> Result<Self, Self::Error> {
MipStoreRaw::try_from((value, cfg)).map(|store_raw| Self(Arc::new(RwLock::new(store_raw))))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MipStatsConfig {
pub block_count_considered: usize,
pub warn_announced_version_ratio: Ratio<u64>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct MipStoreStats {
pub(crate) config: MipStatsConfig,
pub(crate) latest_announcements: VecDeque<u32>,
pub(crate) network_version_counters: HashMap<u32, u64>,
}
impl MipStoreStats {
pub(crate) fn new(config: MipStatsConfig) -> Self {
Self {
config: config.clone(),
latest_announcements: VecDeque::with_capacity(config.block_count_considered),
network_version_counters: HashMap::with_capacity(config.block_count_considered),
}
}
fn reset(&mut self) {
self.latest_announcements.clear();
self.network_version_counters.clear();
}
}
#[derive(Error, Debug, PartialEq)]
pub enum UpdateWithError {
#[error("MipInfo {0:#?} is not consistent with state: {1:#?}, error: {2}")]
NonConsistent(MipInfo, MipState, IsConsistentError),
#[error("For MipInfo {0:?}, trying to downgrade from state {1:?} to {2:?}")]
Downgrade(MipInfo, ComponentState, ComponentState),
#[error("MipInfo {0:?} has overlapping data of MipInfo {1:?}")]
Overlapping(MipInfo, MipInfo),
#[error("MipInfo {0:?} has an invalid activation delay value: {1}, min allowed: {2}")]
InvalidActivationDelay(MipInfo, MassaTime, MassaTime),
}
#[derive(Error, Debug)]
pub enum ExtendFromDbError {
#[error("Unable to get an handle over db column: {0}")]
UnknownDbColumn(String),
#[error("{0}")]
Update(#[from] UpdateWithError),
#[error("{0}")]
Deserialize(String),
}
#[derive(Error, Debug)]
pub enum IsConsistentWithShutdownPeriodError {
#[error("{0}")]
Update(#[from] ModelsError),
#[error("MipInfo: {0:?} (state: {1:?}) is not consistent with shutdown: {2} {3}")]
NonConsistent(MipInfo, ComponentState, MassaTime, MassaTime),
}
#[derive(Error, Debug)]
pub enum IsKVValidError {
#[error("{0}")]
Deserialize(String),
#[error("Invalid prefix for key")]
InvalidPrefix,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MipStoreRaw {
pub(crate) store: BTreeMap<MipInfo, MipState>,
pub(crate) stats: MipStoreStats,
}
impl MipStoreRaw {
#[allow(clippy::result_large_err)]
pub fn update_with(
&mut self,
store_raw: &MipStoreRaw,
) -> Result<(Vec<MipInfo>, BTreeMap<MipInfo, MipState>), UpdateWithError> {
let mut component_versions: HashMap<MipComponent, u32> = self
.store
.iter()
.flat_map(|c| {
c.0.components
.iter()
.map(|(mip_component, component_version)| {
(mip_component.clone(), *component_version)
})
})
.collect();
let mut names: HashSet<String> = self.store.keys().map(|mi| mi.name.clone()).collect();
let mut to_update: BTreeMap<MipInfo, MipState> = Default::default();
let mut to_add: BTreeMap<MipInfo, MipState> = Default::default();
let mut has_error: Option<UpdateWithError> = None;
for (m_info, m_state) in store_raw.store.iter() {
if let Err(e) = m_state.is_consistent_with(m_info) {
has_error = Some(UpdateWithError::NonConsistent(
m_info.clone(),
m_state.clone(),
e,
));
break;
}
if let Some(m_state_orig) = self.store.get(m_info) {
let m_state_id: u32 = ComponentStateTypeId::from(&m_state.state).into();
let m_state_orig_id: u32 = ComponentStateTypeId::from(&m_state_orig.state).into();
if matches!(
m_state_orig.state,
ComponentState::Defined(_)
| ComponentState::Started(_)
| ComponentState::LockedIn(_)
) {
if m_state_id >= m_state_orig_id {
to_update.insert(m_info.clone(), m_state.clone());
} else {
has_error = Some(UpdateWithError::Downgrade(
m_info.clone(),
m_state_orig.state,
m_state.state,
));
break;
}
}
} else {
let last_m_info_ = to_add
.last_key_value()
.map(|(mi, _)| mi)
.or(self.store.last_key_value().map(|(mi, _)| mi));
if let Some(last_m_info) = last_m_info_ {
let mut component_version_compatible = true;
for (component, component_version) in m_info.components.iter() {
if component_version <= component_versions.get(component).unwrap_or(&0) {
component_version_compatible = false;
break;
}
}
#[cfg(not(any(test, feature = "test-exports")))]
if m_info.activation_delay < VERSIONING_ACTIVATION_DELAY_MIN {
has_error = Some(UpdateWithError::InvalidActivationDelay(
m_info.clone(),
m_info.activation_delay,
VERSIONING_ACTIVATION_DELAY_MIN,
));
break;
}
if m_info.start > last_m_info.timeout
&& m_info.timeout > m_info.start
&& m_info.version > last_m_info.version
&& !names.contains(&m_info.name)
&& component_version_compatible
{
to_add.insert(m_info.clone(), m_state.clone());
names.insert(m_info.name.clone());
for (component, component_version) in m_info.components.iter() {
component_versions.insert(component.clone(), *component_version);
}
} else {
has_error = Some(UpdateWithError::Overlapping(
m_info.clone(),
last_m_info.clone(),
));
break;
}
} else {
to_add.insert(m_info.clone(), m_state.clone());
names.insert(m_info.name.clone());
}
}
}
match has_error {
None => {
let updated: Vec<MipInfo> = to_update.keys().cloned().collect();
self.store.append(&mut to_update);
Ok((updated, to_add))
}
Some(e) => Err(e),
}
}
fn update_network_version_stats(
&mut self,
slot_timestamp: MassaTime,
network_versions: Option<(u32, Option<u32>)>,
) {
if let Some((_current_network_version, announced_network_version_)) = network_versions {
let announced_network_version = announced_network_version_.unwrap_or(0);
let removed_version_ = match self.stats.latest_announcements.len() {
n if n >= self.stats.config.block_count_considered => {
self.stats.latest_announcements.pop_front()
}
_ => None,
};
self.stats
.latest_announcements
.push_back(announced_network_version);
let mut network_version_count = *self
.stats
.network_version_counters
.entry(announced_network_version)
.and_modify(|v| *v = v.saturating_add(1))
.or_insert(1);
if let Some(removed_version) = removed_version_ {
if let Entry::Occupied(mut e) =
self.stats.network_version_counters.entry(removed_version)
{
let entry_value = e.get_mut();
*entry_value = entry_value.saturating_sub(1);
network_version_count = *entry_value;
if *entry_value == 0 {
self.stats.network_version_counters.remove(&removed_version);
}
}
}
if announced_network_version != 0 {
let vote_ratio = Ratio::new(
network_version_count,
self.stats.config.block_count_considered as u64,
);
if vote_ratio > self.stats.config.warn_announced_version_ratio {
let last_key_value = self.store.last_key_value();
if let Some((mi, _ms)) = last_key_value {
if announced_network_version > mi.version {
warn!("{} our of {} last blocks advertised that they are willing to transition to version {}. You should update your node if you wish to move to that version.",
network_version_count,
self.stats.config.block_count_considered,
announced_network_version
);
}
}
}
}
}
debug!(
"[VERSIONING STATS] stats have {} counters and {} announcements",
self.stats.network_version_counters.len(),
self.stats.latest_announcements.len()
);
self.advance_states_on_updated_stats(slot_timestamp);
}
fn advance_states_on_updated_stats(&mut self, slot_timestamp: MassaTime) {
for (mi, state) in self.store.iter_mut() {
if state.is_final() {
continue;
}
let network_version_count = *self
.stats
.network_version_counters
.get(&mi.version)
.unwrap_or(&0);
let vote_ratio = Ratio::new(
network_version_count,
self.stats.config.block_count_considered as u64,
);
debug!("[VERSIONING STATS] Vote counts / blocks considered = {} / {} (for MipInfo with network version {} - {})",
network_version_count,
self.stats.config.block_count_considered,
mi.version,
mi.name);
let advance_msg = Advance {
start_timestamp: mi.start,
timeout: mi.timeout,
threshold: vote_ratio,
now: slot_timestamp,
activation_delay: mi.activation_delay,
};
state.on_advance(&advance_msg.clone());
}
}
fn get_latest_component_version_at(&self, component: &MipComponent, ts: MassaTime) -> u32 {
let version = self
.store
.iter()
.rev()
.filter(|(mi, ms)| {
mi.components.contains_key(component)
&& matches!(ms.state, ComponentState::Active(_))
})
.find_map(|(mi, ms)| {
let res = ms.state_at(ts, mi.start, mi.timeout, mi.activation_delay);
match res {
Ok(ComponentStateTypeId::Active) => mi.components.get(component).copied(),
_ => None,
}
})
.unwrap_or(0);
version
}
fn get_all_active_component_versions(&self, component: &MipComponent) -> Vec<u32> {
let versions_iter = self.store.iter().filter_map(|(mi, ms)| {
if matches!(ms.state, ComponentState::Active(_)) {
mi.components.get(component).copied()
} else {
None
}
});
let versions: Vec<u32> = iter::once(0).chain(versions_iter).collect();
versions
}
fn get_all_component_versions(
&self,
component: &MipComponent,
) -> BTreeMap<u32, ComponentStateTypeId> {
let versions_iter = self.store.iter().filter_map(|(mi, ms)| {
mi.components
.get(component)
.copied()
.map(|component_version| (component_version, ComponentStateTypeId::from(&ms.state)))
});
iter::once((0, ComponentStateTypeId::Active))
.chain(versions_iter)
.collect()
}
fn is_consistent_with_shutdown_period(
&self,
shutdown_start: Slot,
shutdown_end: Slot,
thread_count: u8,
t0: MassaTime,
genesis_timestamp: MassaTime,
) -> Result<(), IsConsistentWithShutdownPeriodError> {
let mut has_error: Result<(), IsConsistentWithShutdownPeriodError> = Ok(());
let shutdown_start_ts =
get_block_slot_timestamp(thread_count, t0, genesis_timestamp, shutdown_start)?;
let shutdown_end_ts =
get_block_slot_timestamp(thread_count, t0, genesis_timestamp, shutdown_end)?;
let shutdown_range = shutdown_start_ts..=shutdown_end_ts;
for (mip_info, mip_state) in &self.store {
match mip_state.state {
ComponentState::Defined(..) => {
if shutdown_range.contains(&mip_info.start)
|| shutdown_range.contains(&mip_info.timeout)
{
has_error = Err(IsConsistentWithShutdownPeriodError::NonConsistent(
mip_info.clone(),
mip_state.state,
shutdown_start_ts,
shutdown_end_ts,
));
break;
}
}
ComponentState::Started(..) | ComponentState::LockedIn(..) => {
has_error = Err(IsConsistentWithShutdownPeriodError::NonConsistent(
mip_info.clone(),
mip_state.state,
shutdown_start_ts,
shutdown_end_ts,
));
break;
}
_ => {
}
}
}
has_error
}
fn update_for_network_shutdown(
&mut self,
shutdown_start: Slot,
shutdown_end: Slot,
thread_count: u8,
t0: MassaTime,
genesis_timestamp: MassaTime,
) -> Result<(), ModelsError> {
let shutdown_start_ts =
get_block_slot_timestamp(thread_count, t0, genesis_timestamp, shutdown_start)?;
let shutdown_end_ts =
get_block_slot_timestamp(thread_count, t0, genesis_timestamp, shutdown_end)?;
let shutdown_range = shutdown_start_ts..=shutdown_end_ts;
let mut new_store: BTreeMap<MipInfo, MipState> = Default::default();
let mut new_stats = self.stats.clone();
new_stats.reset();
let next_valid_start_ = shutdown_end.get_next_slot(thread_count)?;
let next_valid_start =
get_block_slot_timestamp(thread_count, t0, genesis_timestamp, next_valid_start_)?;
let mut offset: Option<MassaTime> = None;
for (mip_info, mip_state) in &self.store {
match mip_state.state {
ComponentState::Defined(..) => {
let mut new_mip_info = mip_info.clone();
if shutdown_range.contains(&new_mip_info.start) {
let offset_ts = match offset {
Some(offset_ts) => offset_ts,
None => {
let offset_ts = next_valid_start.saturating_sub(mip_info.start);
offset = Some(offset_ts);
offset_ts
}
};
new_mip_info.start = new_mip_info.start.saturating_add(offset_ts);
new_mip_info.timeout = new_mip_info
.start
.saturating_add(mip_info.timeout.saturating_sub(mip_info.start));
}
new_store.insert(new_mip_info, mip_state.clone());
}
ComponentState::Started(..) | ComponentState::LockedIn(..) => {
let mut new_mip_info = mip_info.clone();
let offset_ts = match offset {
Some(offset_ts) => offset_ts,
None => {
let offset_ts = next_valid_start.saturating_sub(mip_info.start);
offset = Some(offset_ts);
offset_ts
}
};
new_mip_info.start = new_mip_info.start.saturating_add(offset_ts);
new_mip_info.timeout = new_mip_info
.start
.saturating_add(mip_info.timeout.saturating_sub(mip_info.start));
let new_mip_state = MipState::reset_from(mip_state)
.ok_or(ModelsError::from("Unable to reset state"))?;
new_store.insert(new_mip_info, new_mip_state.clone());
}
_ => {
new_store.insert(mip_info.clone(), mip_state.clone());
}
}
}
self.store = new_store;
self.stats = new_stats;
Ok(())
}
pub fn is_key_value_valid(&self, serialized_key: &[u8], serialized_value: &[u8]) -> bool {
self._is_key_value_valid(serialized_key, serialized_value)
.is_ok()
}
pub fn _is_key_value_valid(
&self,
serialized_key: &[u8],
serialized_value: &[u8],
) -> Result<(), IsKVValidError> {
let mip_info_deser = MipInfoDeserializer::new();
let mip_state_deser = MipStateDeserializer::new();
let mip_store_stats_deser = MipStoreStatsDeserializer::new(
MIP_STORE_STATS_BLOCK_CONSIDERED,
self.stats.config.warn_announced_version_ratio,
);
if serialized_key.starts_with(MIP_STORE_PREFIX.as_bytes()) {
let (rem, _mip_info) = mip_info_deser
.deserialize::<DeserializeError>(&serialized_key[MIP_STORE_PREFIX.len()..])
.map_err(|e| IsKVValidError::Deserialize(e.to_string()))?;
if !rem.is_empty() {
return Err(IsKVValidError::Deserialize(
"Rem not empty after deserialization".to_string(),
));
}
let (rem2, _mip_state) = mip_state_deser
.deserialize::<DeserializeError>(serialized_value)
.map_err(|e| IsKVValidError::Deserialize(e.to_string()))?;
if !rem2.is_empty() {
return Err(IsKVValidError::Deserialize(
"Rem not empty after deserialization".to_string(),
));
}
} else if serialized_key.starts_with(MIP_STORE_STATS_PREFIX.as_bytes()) {
let (rem, _mip_store_stats) = mip_store_stats_deser
.deserialize::<DeserializeError>(serialized_value)
.map_err(|e| IsKVValidError::Deserialize(e.to_string()))?;
if !rem.is_empty() {
return Err(IsKVValidError::Deserialize(
"Rem not empty after deserialization".to_string(),
));
}
} else {
return Err(IsKVValidError::InvalidPrefix);
}
Ok(())
}
fn update_batches(
&self,
batch: &mut DBBatch,
versioning_batch: &mut DBBatch,
between: Option<(&MassaTime, &MassaTime)>,
) -> Result<(), SerializeError> {
let mip_info_ser = MipInfoSerializer::new();
let mip_state_ser = MipStateSerializer::new();
let bounds = match between {
Some(between) => (*between.0)..(*between.1),
None => MassaTime::from_millis(0)..MassaTime::max(),
};
let mut key = Vec::new();
let mut value = Vec::new();
for (mip_info, mip_state) in self.store.iter() {
if let Some((advance, state_id)) = mip_state.history.last_key_value() {
if bounds.contains(&advance.now) {
key.extend(MIP_STORE_PREFIX.as_bytes().to_vec());
mip_info_ser.serialize(mip_info, &mut key)?;
mip_state_ser.serialize(mip_state, &mut value)?;
match state_id {
ComponentStateTypeId::Active => {
batch.insert(key.clone(), Some(value.clone()));
versioning_batch.insert(key.clone(), None);
}
_ => {
versioning_batch.insert(key.clone(), Some(value.clone()));
}
}
key.clear();
value.clear();
}
}
}
value.clear();
let mip_stats_ser = MipStoreStatsSerializer::new();
mip_stats_ser.serialize(&self.stats, &mut value)?;
versioning_batch.insert(
MIP_STORE_STATS_PREFIX.as_bytes().to_vec(),
Some(value.clone()),
);
Ok(())
}
fn extend_from_db(
&mut self,
db: ShareableMassaDBController,
) -> Result<(Vec<MipInfo>, BTreeMap<MipInfo, MipState>), ExtendFromDbError> {
let mip_info_deser = MipInfoDeserializer::new();
let mip_state_deser = MipStateDeserializer::new();
let mip_store_stats_deser = MipStoreStatsDeserializer::new(
MIP_STORE_STATS_BLOCK_CONSIDERED,
self.stats.config.warn_announced_version_ratio,
);
let db = db.read();
let mut update_data: BTreeMap<MipInfo, MipState> = Default::default();
for (ser_mip_info, ser_mip_state) in
db.prefix_iterator_cf(STATE_CF, MIP_STORE_PREFIX.as_bytes())
{
if !ser_mip_info.starts_with(MIP_STORE_PREFIX.as_bytes()) {
break;
}
let (_, mip_info) = mip_info_deser
.deserialize::<DeserializeError>(&ser_mip_info[MIP_STORE_PREFIX.len()..])
.map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
let (_, mip_state) = mip_state_deser
.deserialize::<DeserializeError>(&ser_mip_state)
.map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
update_data.insert(mip_info, mip_state);
}
let (mut updated, mut added) = match update_data.is_empty() {
true => (vec![], BTreeMap::new()),
false => {
let store_raw_ = MipStoreRaw {
store: update_data,
stats: MipStoreStats {
config: MipStatsConfig {
block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
warn_announced_version_ratio: self
.stats
.config
.warn_announced_version_ratio,
},
latest_announcements: Default::default(),
network_version_counters: Default::default(),
},
};
self.update_with(&store_raw_)?
}
};
let mut update_data: BTreeMap<MipInfo, MipState> = Default::default();
for (ser_mip_info, ser_mip_state) in
db.prefix_iterator_cf(VERSIONING_CF, MIP_STORE_PREFIX.as_bytes())
{
match &ser_mip_info {
key if key.starts_with(MIP_STORE_PREFIX.as_bytes()) => {
let (_, mip_info) = mip_info_deser
.deserialize::<DeserializeError>(&ser_mip_info[MIP_STORE_PREFIX.len()..])
.map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
let (_, mip_state) = mip_state_deser
.deserialize::<DeserializeError>(&ser_mip_state)
.map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
update_data.insert(mip_info, mip_state);
}
key if key.starts_with(MIP_STORE_STATS_PREFIX.as_bytes()) => {
let (_, mip_store_stats) = mip_store_stats_deser
.deserialize::<DeserializeError>(&ser_mip_state)
.map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
self.stats = mip_store_stats;
}
_ => {
break;
}
}
}
if !update_data.is_empty() {
let store_raw_ = MipStoreRaw {
store: update_data,
stats: MipStoreStats {
config: MipStatsConfig {
block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
warn_announced_version_ratio: self
.stats
.config
.warn_announced_version_ratio,
},
latest_announcements: Default::default(),
network_version_counters: Default::default(),
},
};
let (updated_2, added_2) = self.update_with(&store_raw_)?;
updated.extend(updated_2);
added.extend(added_2);
}
Ok((updated, added))
}
fn try_from_db(
db: ShareableMassaDBController,
cfg: MipStatsConfig,
) -> Result<Self, ExtendFromDbError> {
let mut store_raw = MipStoreRaw {
store: Default::default(),
stats: MipStoreStats {
config: cfg,
latest_announcements: Default::default(),
network_version_counters: Default::default(),
},
};
let (_updated, mut added) = store_raw.extend_from_db(db)?;
store_raw.store.append(&mut added);
Ok(store_raw)
}
}
impl<const N: usize> TryFrom<([(MipInfo, MipState); N], MipStatsConfig)> for MipStoreRaw {
type Error = UpdateWithError;
fn try_from(
(value, cfg): ([(MipInfo, MipState); N], MipStatsConfig),
) -> Result<Self, Self::Error> {
let mut store = Self {
store: Default::default(),
stats: MipStoreStats::new(cfg.clone()),
};
let other_store = Self {
store: BTreeMap::from(value),
stats: MipStoreStats::new(cfg),
};
match store.update_with(&other_store) {
Ok((_updated, mut added)) => {
store.store.append(&mut added);
Ok(store)
}
Err(e) => Err(e),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use assert_matches::assert_matches;
use massa_db_exports::{MassaDBConfig, MassaDBController, MassaIteratorMode};
use massa_db_worker::MassaDB;
use more_asserts::{assert_gt, assert_le};
use parking_lot::RwLock;
use std::ops::{Add, Sub};
use std::sync::Arc;
use tempfile::tempdir;
use crate::test_helpers::versioning_helpers::advance_state_until;
use massa_models::config::{MIP_STORE_STATS_BLOCK_CONSIDERED, T0, THREAD_COUNT};
use massa_models::timeslots::get_closest_slot_to_timestamp;
impl PartialEq<ComponentState> for MipState {
fn eq(&self, other: &ComponentState) -> bool {
self.state == *other
}
}
impl From<(&MipInfo, &Ratio<u64>, &MassaTime)> for Advance {
fn from((mip_info, threshold, now): (&MipInfo, &Ratio<u64>, &MassaTime)) -> Self {
Self {
start_timestamp: mip_info.start,
timeout: mip_info.timeout,
threshold: *threshold,
now: *now,
activation_delay: mip_info.activation_delay,
}
}
}
fn get_a_version_info() -> (MassaTime, MassaTime, MipInfo) {
let start = MassaTime::from_utc_ymd_hms(2017, 11, 1, 7, 33, 44).unwrap();
let timeout = MassaTime::from_utc_ymd_hms(2017, 11, 11, 7, 33, 44).unwrap();
(
start,
timeout,
MipInfo {
name: "MIP-0002".to_string(),
version: 2,
components: BTreeMap::from([(MipComponent::Address, 1)]),
start,
timeout,
activation_delay: MassaTime::from_millis(20),
},
)
}
#[test]
fn test_state_advance_from_defined() {
let (_, _, mi) = get_a_version_info();
let mut state: ComponentState = Default::default();
assert_eq!(state, ComponentState::defined());
let now = mi.start.saturating_sub(MassaTime::from_millis(1));
let mut advance_msg = Advance::from((&mi, &Ratio::zero(), &now));
state = state.on_advance(advance_msg.clone());
assert_eq!(state, ComponentState::defined());
let now = mi.start.saturating_add(MassaTime::from_millis(5));
advance_msg.now = now;
state = state.on_advance(advance_msg);
assert_eq!(
state,
ComponentState::Started(Started {
vote_ratio: Ratio::zero()
})
);
}
#[test]
fn test_state_advance_from_started() {
let (_, _, mi) = get_a_version_info();
let mut state: ComponentState = ComponentState::started(Default::default());
let now = mi.start;
let threshold_too_low =
VERSIONING_THRESHOLD_TRANSITION_ACCEPTED.sub(Ratio::new_raw(10, 100));
let threshold_ok = VERSIONING_THRESHOLD_TRANSITION_ACCEPTED.add(Ratio::new_raw(1, 100));
assert_le!(threshold_ok, Ratio::from_integer(1));
let mut advance_msg = Advance::from((&mi, &threshold_too_low, &now));
state = state.on_advance(advance_msg.clone());
assert_eq!(state, ComponentState::started(threshold_too_low));
advance_msg.threshold = threshold_ok;
state = state.on_advance(advance_msg);
assert_eq!(state, ComponentState::locked_in(now));
}
#[test]
fn test_state_advance_from_locked_in() {
let (_, _, mi) = get_a_version_info();
let locked_in_at = mi.start.saturating_add(MassaTime::from_millis(1));
let mut state: ComponentState = ComponentState::locked_in(locked_in_at);
let now = mi.start;
let mut advance_msg = Advance::from((&mi, &Ratio::zero(), &now));
state = state.on_advance(advance_msg.clone());
assert_eq!(state, ComponentState::locked_in(locked_in_at));
advance_msg.now = advance_msg
.timeout
.saturating_add(MassaTime::from_millis(1));
state = state.on_advance(advance_msg);
assert!(matches!(state, ComponentState::Active(_)));
}
#[test]
fn test_state_advance_from_active() {
let (start, _, mi) = get_a_version_info();
let mut state = ComponentState::active(start);
let now = mi.start;
let advance = Advance::from((&mi, &Ratio::zero(), &now));
state = state.on_advance(advance);
assert!(matches!(state, ComponentState::Active(_)));
}
#[test]
fn test_state_advance_from_failed() {
let (_, _, mi) = get_a_version_info();
let mut state = ComponentState::failed();
let now = mi.start;
let advance = Advance::from((&mi, &Ratio::zero(), &now));
state = state.on_advance(advance);
assert_eq!(state, ComponentState::failed());
}
#[test]
fn test_state_advance_to_failed() {
let (_, _, mi) = get_a_version_info();
let now = mi.timeout.saturating_add(MassaTime::from_millis(1));
let advance_msg = Advance::from((&mi, &Ratio::zero(), &now));
let mut state: ComponentState = Default::default(); state = state.on_advance(advance_msg.clone());
assert_eq!(state, ComponentState::Failed(Failed {}));
let mut state = ComponentState::started(Default::default());
state = state.on_advance(advance_msg.clone());
assert_eq!(state, ComponentState::Failed(Failed {}));
}
#[test]
fn test_state_with_history() {
let (start, _, mi) = get_a_version_info();
let now_0 = start;
let mut state = MipState::new(now_0);
assert_eq!(state, ComponentState::defined());
let now = mi.start.saturating_add(MassaTime::from_millis(15));
let mut advance_msg = Advance::from((&mi, &Ratio::zero(), &now));
state.on_advance(&advance_msg);
assert_eq!(state, ComponentState::started(Ratio::zero()));
assert_eq!(state.history.len(), 2);
assert!(matches!(
state.history.first_key_value(),
Some((&AdvanceLW { .. }, &ComponentStateTypeId::Defined))
));
assert!(matches!(
state.history.last_key_value(),
Some((&AdvanceLW { .. }, &ComponentStateTypeId::Started))
));
let state_id_ = state.state_at(
mi.start.saturating_sub(MassaTime::from_millis(5)),
mi.start,
mi.timeout,
mi.activation_delay,
);
assert!(matches!(
state_id_,
Err(StateAtError::BeforeInitialState(_, _))
));
let state_id = state
.state_at(mi.start, mi.start, mi.timeout, mi.activation_delay)
.unwrap();
assert_eq!(state_id, ComponentStateTypeId::Defined);
let state_id = state
.state_at(now, mi.start, mi.timeout, mi.activation_delay)
.unwrap();
assert_eq!(state_id, ComponentStateTypeId::Started);
let after_started_ts = now.saturating_add(MassaTime::from_millis(15));
let state_id_ = state.state_at(after_started_ts, mi.start, mi.timeout, mi.activation_delay);
assert_eq!(state_id_, Err(StateAtError::Unpredictable));
let after_timeout_ts = mi.timeout.saturating_add(MassaTime::from_millis(15));
let state_id = state
.state_at(after_timeout_ts, mi.start, mi.timeout, mi.activation_delay)
.unwrap();
assert_eq!(state_id, ComponentStateTypeId::Failed);
let threshold = VERSIONING_THRESHOLD_TRANSITION_ACCEPTED;
advance_msg.threshold = threshold.add(Ratio::from_integer(1));
advance_msg.now = now.saturating_add(MassaTime::from_millis(1));
state.on_advance(&advance_msg);
assert_eq!(state, ComponentState::locked_in(advance_msg.now));
let after_locked_in_ts = now.saturating_add(MassaTime::from_millis(10));
let state_id = state
.state_at(
after_locked_in_ts,
mi.start,
mi.timeout,
mi.activation_delay,
)
.unwrap();
assert_eq!(state_id, ComponentStateTypeId::LockedIn);
let state_id = state
.state_at(after_timeout_ts, mi.start, mi.timeout, mi.activation_delay)
.unwrap();
assert_eq!(state_id, ComponentStateTypeId::Active);
}
#[test]
fn test_versioning_store_announce_current() {
let (start, timeout, mi) = get_a_version_info();
let mut mi_2 = mi.clone();
mi_2.version += 1;
mi_2.start = timeout
.checked_add(MassaTime::from_millis(1000 * 60 * 60 * 24 * 2))
.unwrap(); mi_2.timeout = timeout
.checked_add(MassaTime::from_millis(1000 * 60 * 60 * 24 * 5))
.unwrap(); let vs_1 = MipState {
state: ComponentState::active(start),
history: Default::default(),
};
let vs_2 = MipState {
state: ComponentState::started(Ratio::zero()),
history: Default::default(),
};
let mip_stats_cfg = MipStatsConfig {
block_count_considered: 10,
warn_announced_version_ratio: Ratio::new_raw(30, 100),
};
let vs_raw = MipStoreRaw {
store: BTreeMap::from([(mi.clone(), vs_1), (mi_2.clone(), vs_2)]),
stats: MipStoreStats::new(mip_stats_cfg.clone()),
};
let vs = MipStore(Arc::new(RwLock::new(vs_raw)));
assert_eq!(vs.get_network_version_current(), mi.version);
assert_eq!(vs.get_network_version_to_announce(), Some(mi_2.version));
let vs_raw = MipStoreRaw {
store: Default::default(),
stats: MipStoreStats::new(mip_stats_cfg),
};
let vs = MipStore(Arc::new(RwLock::new(vs_raw)));
assert_eq!(vs.get_network_version_current(), 0);
assert_eq!(vs.get_network_version_to_announce(), None);
}
#[test]
fn test_is_consistent_with() {
let vi_1 = MipInfo {
name: "MIP-0002".to_string(),
version: 2,
components: BTreeMap::from([(MipComponent::Address, 1)]),
start: MassaTime::from_millis(2),
timeout: MassaTime::from_millis(5),
activation_delay: MassaTime::from_millis(2),
};
let vi_2 = MipInfo {
name: "MIP-0002".to_string(),
version: 2,
components: BTreeMap::from([(MipComponent::Address, 1)]),
start: MassaTime::from_millis(7),
timeout: MassaTime::from_millis(10),
activation_delay: MassaTime::from_millis(2),
};
let vsh = MipState {
state: ComponentState::Error,
history: Default::default(),
};
assert_eq!(
vsh.is_consistent_with(&vi_1),
Err(IsConsistentError::AtError)
);
let vsh = MipState {
state: ComponentState::defined(),
history: Default::default(),
};
assert!(vsh.is_consistent_with(&vi_1).is_err());
let mut vsh = MipState::new(MassaTime::from_millis(1));
assert!(vsh.is_consistent_with(&vi_1).is_ok());
let now = MassaTime::from_millis(3);
let adv = Advance::from((&vi_1, &Ratio::zero(), &now));
vsh.on_advance(&adv);
let now = MassaTime::from_millis(4);
let adv = Advance::from((&vi_1, &Ratio::new_raw(14, 100), &now));
vsh.on_advance(&adv);
assert_eq!(vsh.state, ComponentState::started(Ratio::new_raw(14, 100)));
assert!(vsh.is_consistent_with(&vi_1).is_ok());
assert!(vsh.is_consistent_with(&vi_2).is_err());
let now = MassaTime::from_millis(4);
let adv = Advance::from((&vi_1, &VERSIONING_THRESHOLD_TRANSITION_ACCEPTED, &now));
vsh.on_advance(&adv);
assert_eq!(vsh.state, ComponentState::locked_in(now));
assert!(vsh.is_consistent_with(&vi_1).is_ok());
}
#[test]
fn test_update_with() {
let vi_1 = MipInfo {
name: "MIP-0002".to_string(),
version: 2,
components: BTreeMap::from([(MipComponent::Address, 1)]),
start: MassaTime::from_millis(2),
timeout: MassaTime::from_millis(5),
activation_delay: MassaTime::from_millis(2),
};
let _time = MassaTime::now();
let vs_1 = advance_state_until(ComponentState::active(_time), &vi_1);
assert!(matches!(vs_1.state, ComponentState::Active(_)));
let vi_2 = MipInfo {
name: "MIP-0003".to_string(),
version: 3,
components: BTreeMap::from([(MipComponent::Address, 2)]),
start: MassaTime::from_millis(17),
timeout: MassaTime::from_millis(27),
activation_delay: MassaTime::from_millis(2),
};
let vs_2 = advance_state_until(ComponentState::defined(), &vi_2);
let mip_stats_cfg = MipStatsConfig {
block_count_considered: 10,
warn_announced_version_ratio: Ratio::new_raw(30, 100),
};
let mut vs_raw_1 = MipStoreRaw::try_from((
[(vi_1.clone(), vs_1.clone()), (vi_2.clone(), vs_2.clone())],
mip_stats_cfg.clone(),
))
.unwrap();
let vs_2_2 = advance_state_until(ComponentState::active(_time), &vi_2);
assert!(matches!(vs_2_2.state, ComponentState::Active(_)));
let vs_raw_2 = MipStoreRaw::try_from((
[(vi_1.clone(), vs_1.clone()), (vi_2.clone(), vs_2_2.clone())],
mip_stats_cfg,
))
.unwrap();
let (updated, added) = vs_raw_1.update_with(&vs_raw_2).unwrap();
assert!(added.is_empty());
assert_eq!(updated, vec![vi_2.clone()]);
assert_eq!(vs_raw_1.store.get(&vi_1).unwrap().state, vs_1.state);
assert_eq!(vs_raw_1.store.get(&vi_2).unwrap().state, vs_2_2.state);
}
#[test]
fn test_update_with_invalid() {
let mi_1 = MipInfo {
name: "MIP-0001".to_string(),
version: 2,
components: BTreeMap::from([(MipComponent::Address, 1)]),
start: MassaTime::from_millis(0),
timeout: MassaTime::from_millis(5),
activation_delay: MassaTime::from_millis(2),
};
let _time = MassaTime::now();
let ms_1 = advance_state_until(ComponentState::active(_time), &mi_1);
assert!(matches!(ms_1.state, ComponentState::Active(_)));
let mi_2 = MipInfo {
name: "MIP-0002".to_string(),
version: 3,
components: BTreeMap::from([(MipComponent::Address, 2)]),
start: MassaTime::from_millis(17),
timeout: MassaTime::from_millis(27),
activation_delay: MassaTime::from_millis(2),
};
let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
assert_eq!(ms_2, ComponentState::defined());
let mip_stats_cfg = MipStatsConfig {
block_count_considered: 10,
warn_announced_version_ratio: Ratio::new_raw(30, 100),
};
{
let mut store_1 = MipStoreRaw::try_from((
[(mi_1.clone(), ms_1.clone()), (mi_2.clone(), ms_2.clone())],
mip_stats_cfg.clone(),
))
.unwrap();
let mut mi_2_2 = mi_2.clone();
mi_2_2.start = mi_1.timeout;
let ms_2_2 = advance_state_until(ComponentState::defined(), &mi_2_2);
let store_2 = MipStoreRaw {
store: BTreeMap::from([
(mi_1.clone(), ms_1.clone()),
(mi_2_2.clone(), ms_2_2.clone()),
]),
stats: MipStoreStats::new(mip_stats_cfg.clone()),
};
assert_matches!(
store_1.update_with(&store_2),
Err(UpdateWithError::Overlapping(..))
);
assert_eq!(store_1.store.get(&mi_1).unwrap().state, ms_1.state);
assert_eq!(store_1.store.get(&mi_2).unwrap().state, ms_2.state);
{
let _store_2_ = MipStoreRaw::try_from((
[
(mi_1.clone(), ms_1.clone()),
(mi_2_2.clone(), ms_2_2.clone()),
],
mip_stats_cfg.clone(),
));
assert!(_store_2_.is_err());
}
}
{
let mut store_1 = MipStoreRaw::try_from((
[(mi_1.clone(), ms_1.clone()), (mi_2.clone(), ms_2.clone())],
mip_stats_cfg.clone(),
))
.unwrap();
let mut mi_2_2 = mi_2.clone();
mi_2_2.components = mi_1.components.clone();
let ms_2_2 = advance_state_until(ComponentState::defined(), &mi_2_2);
let store_2 = MipStoreRaw {
store: BTreeMap::from([
(mi_1.clone(), ms_1.clone()),
(mi_2_2.clone(), ms_2_2.clone()),
]),
stats: MipStoreStats::new(mip_stats_cfg.clone()),
};
assert_matches!(
store_1.update_with(&store_2),
Err(UpdateWithError::Overlapping(..))
);
}
{
let mut store_1 =
MipStoreRaw::try_from(([(mi_1.clone(), ms_1.clone())], mip_stats_cfg.clone()))
.unwrap();
let mut mi_2_2 = mi_2.clone();
mi_2_2.version = mi_1.version - 1;
let store_2 = MipStoreRaw {
store: BTreeMap::from([(mi_2_2.clone(), ms_2.clone())]),
stats: MipStoreStats::new(mip_stats_cfg.clone()),
};
assert_matches!(
store_1.update_with(&store_2),
Err(UpdateWithError::Overlapping(..))
);
let mut mi_2_3 = mi_2.clone();
mi_2_3.version = mi_1.version;
let store_2 = MipStoreRaw {
store: BTreeMap::from([(mi_2_3.clone(), ms_2.clone())]),
stats: MipStoreStats::new(mip_stats_cfg.clone()),
};
assert_matches!(
store_1.update_with(&store_2),
Err(UpdateWithError::Overlapping(..))
);
}
{
let mut store_1 =
MipStoreRaw::try_from(([(mi_1.clone(), ms_1.clone())], mip_stats_cfg.clone()))
.unwrap();
let mut mi_2_2 = mi_2.clone();
mi_2_2.name = mi_1.name.clone();
let store_2 = MipStoreRaw {
store: BTreeMap::from([(mi_2_2.clone(), ms_2.clone())]),
stats: MipStoreStats::new(mip_stats_cfg.clone()),
};
assert_matches!(
store_1.update_with(&store_2),
Err(UpdateWithError::Overlapping(..))
);
}
{
let ms_1_1 =
advance_state_until(ComponentState::locked_in(MassaTime::from_millis(0)), &mi_1);
let ms_1_2 = advance_state_until(ComponentState::started(Ratio::zero()), &mi_1);
let mut store_1 =
MipStoreRaw::try_from(([(mi_1.clone(), ms_1_1.clone())], mip_stats_cfg.clone()))
.unwrap();
let store_2 = MipStoreRaw {
store: BTreeMap::from([(mi_1.clone(), ms_1_2.clone())]),
stats: MipStoreStats::new(mip_stats_cfg.clone()),
};
assert_matches!(
store_1.update_with(&store_2),
Err(UpdateWithError::Downgrade(..))
);
}
}
#[test]
fn test_try_from_invalid() {
let mip_stats_cfg = MipStatsConfig {
block_count_considered: 10,
warn_announced_version_ratio: Ratio::new_raw(30, 100),
};
let mi_1 = MipInfo {
name: "MIP-0002".to_string(),
version: 2,
components: BTreeMap::from([(MipComponent::Address, 1)]),
start: MassaTime::from_millis(0),
timeout: MassaTime::from_millis(5),
activation_delay: MassaTime::from_millis(2),
};
let _time = MassaTime::now();
let ms_1 = advance_state_until(ComponentState::active(_time), &mi_1);
assert!(matches!(ms_1.state, ComponentState::Active(_)));
{
let mut mi_1_1 = mi_1.clone();
mi_1_1.start = MassaTime::from_millis(5);
mi_1_1.timeout = MassaTime::from_millis(2);
let mip_store =
MipStoreRaw::try_from(([(mi_1_1, ms_1.clone())], mip_stats_cfg.clone()));
assert_matches!(mip_store, Err(UpdateWithError::NonConsistent(..)));
}
{
let ms_1_2 = MipState::new(MassaTime::from_millis(15));
let mut mi_1_2 = mi_1.clone();
mi_1_2.start = MassaTime::from_millis(2);
mi_1_2.timeout = MassaTime::from_millis(5);
let mip_store = MipStoreRaw::try_from(([(mi_1_2, ms_1_2)], mip_stats_cfg.clone()));
assert_matches!(mip_store, Err(UpdateWithError::NonConsistent(..)));
}
{
let ms_1_2 = MipState::new(MassaTime::from_millis(15));
let mut mi_1_2 = mi_1.clone();
mi_1_2.start = MassaTime::from_millis(16);
mi_1_2.timeout = MassaTime::from_millis(5);
let mip_store = MipStoreRaw::try_from(([(mi_1_2, ms_1_2)], mip_stats_cfg));
assert_matches!(mip_store, Err(UpdateWithError::NonConsistent(..)));
}
}
#[test]
fn test_empty_mip_store() {
let mip_stats_config = MipStatsConfig {
block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
warn_announced_version_ratio: Ratio::new_raw(30, 100),
};
let mip_store = MipStore::try_from(([], mip_stats_config));
assert!(mip_store.is_ok());
}
#[test]
fn test_update_with_unknown() {
let mip_stats_config = MipStatsConfig {
block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
warn_announced_version_ratio: Ratio::new_raw(30, 100),
};
let mut mip_store_raw_1 = MipStoreRaw::try_from(([], mip_stats_config.clone())).unwrap();
let mi_1 = MipInfo {
name: "MIP-0002".to_string(),
version: 2,
components: BTreeMap::from([(MipComponent::__Nonexhaustive, 1)]),
start: MassaTime::from_millis(0),
timeout: MassaTime::from_millis(5),
activation_delay: MassaTime::from_millis(2),
};
let ms_1 = advance_state_until(ComponentState::defined(), &mi_1);
assert_eq!(ms_1, ComponentState::defined());
let mip_store_raw_2 = MipStoreRaw {
store: BTreeMap::from([(mi_1.clone(), ms_1.clone())]),
stats: MipStoreStats::new(mip_stats_config.clone()),
};
let (updated, added) = mip_store_raw_1.update_with(&mip_store_raw_2).unwrap();
assert_eq!(updated.len(), 0);
assert_eq!(added.len(), 1);
assert_eq!(added.get(&mi_1).unwrap().state, ComponentState::defined());
}
#[test]
fn test_mip_store_network_restart() {
let genesis_timestamp = MassaTime::from_millis(0);
let get_slot_ts =
|slot| get_block_slot_timestamp(THREAD_COUNT, T0, genesis_timestamp, slot).unwrap();
let is_consistent = |store: &MipStoreRaw, shutdown_start, shutdown_end| {
store.is_consistent_with_shutdown_period(
shutdown_start,
shutdown_end,
THREAD_COUNT,
T0,
genesis_timestamp,
)
};
let update_store = |store: &mut MipStoreRaw, shutdown_start, shutdown_end| {
store
.update_for_network_shutdown(
shutdown_start,
shutdown_end,
THREAD_COUNT,
T0,
genesis_timestamp,
)
.unwrap()
};
let _dump_store = |store: &MipStoreRaw| {
println!("Dump store:");
for (mip_info, mip_state) in store.store.iter() {
println!(
"mip_info {} {} - start: {} - timeout: {}: state: {:?}",
mip_info.name,
mip_info.version,
get_closest_slot_to_timestamp(
THREAD_COUNT,
T0,
genesis_timestamp,
mip_info.start
),
get_closest_slot_to_timestamp(
THREAD_COUNT,
T0,
genesis_timestamp,
mip_info.timeout
),
mip_state.state
);
}
};
let shutdown_start = Slot::new(2, 0);
let shutdown_end = Slot::new(8, 0);
let mip_stats_cfg = MipStatsConfig {
block_count_considered: 10,
warn_announced_version_ratio: Ratio::new_raw(30, 100),
};
let mut mi_1 = MipInfo {
name: "MIP-0002".to_string(),
version: 2,
components: BTreeMap::from([(MipComponent::Address, 1)]),
start: MassaTime::from_millis(2),
timeout: MassaTime::from_millis(5),
activation_delay: MassaTime::from_millis(100),
};
let mut mi_2 = MipInfo {
name: "MIP-0003".to_string(),
version: 3,
components: BTreeMap::from([(MipComponent::Address, 2)]),
start: MassaTime::from_millis(7),
timeout: MassaTime::from_millis(11),
activation_delay: MassaTime::from_millis(100),
};
{
mi_1.start = get_slot_ts(Slot::new(3, 7));
mi_1.timeout = get_slot_ts(Slot::new(5, 7));
mi_2.start = get_slot_ts(Slot::new(7, 7));
mi_2.timeout = get_slot_ts(Slot::new(10, 7));
let ms_1 = advance_state_until(ComponentState::defined(), &mi_1);
let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
let mut store = MipStoreRaw::try_from((
[(mi_1.clone(), ms_1), (mi_2.clone(), ms_2)],
mip_stats_cfg.clone(),
))
.unwrap();
match is_consistent(&store, shutdown_start, shutdown_end) {
Err(IsConsistentWithShutdownPeriodError::NonConsistent(mi, ..)) => {
assert_eq!(mi, mi_1);
}
_ => panic!("is_consistent expects a non consistent error"),
}
update_store(&mut store, shutdown_start, shutdown_end);
assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
}
{
mi_1.start = get_slot_ts(Slot::new(9, 7));
mi_1.timeout = get_slot_ts(Slot::new(11, 7));
mi_2.start = get_slot_ts(Slot::new(12, 7));
mi_2.timeout = get_slot_ts(Slot::new(19, 7));
let ms_1 = advance_state_until(ComponentState::defined(), &mi_1);
let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
let mut store = MipStoreRaw::try_from((
[(mi_1.clone(), ms_1), (mi_2.clone(), ms_2)],
mip_stats_cfg.clone(),
))
.unwrap();
let store_orig = store.clone();
assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
update_store(&mut store, shutdown_start, shutdown_end);
assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
assert_eq!(store_orig, store);
}
{
mi_1.start = get_slot_ts(Slot::new(1, 7));
mi_1.timeout = get_slot_ts(Slot::new(5, 7));
mi_2.start = get_slot_ts(Slot::new(7, 7));
mi_2.timeout = get_slot_ts(Slot::new(10, 7));
let ms_1 = advance_state_until(ComponentState::started(Ratio::zero()), &mi_1);
let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
let mut store = MipStoreRaw::try_from((
[(mi_1.clone(), ms_1), (mi_2.clone(), ms_2)],
mip_stats_cfg.clone(),
))
.unwrap();
assert_matches!(
is_consistent(&store, shutdown_start, shutdown_end),
Err(IsConsistentWithShutdownPeriodError::NonConsistent(..))
);
update_store(&mut store, shutdown_start, shutdown_end);
assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
}
{
let shutdown_range = shutdown_start..=shutdown_end;
mi_1.start = get_slot_ts(Slot::new(1, 7));
mi_1.timeout = get_slot_ts(Slot::new(5, 7));
let locked_in_at = Slot::new(1, 9);
assert!(locked_in_at < shutdown_start);
let activate_at = Slot::new(4, 0);
assert!(shutdown_range.contains(&activate_at));
mi_1.activation_delay =
get_slot_ts(activate_at).saturating_sub(get_slot_ts(locked_in_at));
let ms_1 = advance_state_until(
ComponentState::locked_in(get_slot_ts(Slot::new(1, 9))),
&mi_1,
);
mi_2.start = get_slot_ts(Slot::new(7, 7));
mi_2.timeout = get_slot_ts(Slot::new(10, 7));
let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
let mut store = MipStoreRaw::try_from((
[(mi_1.clone(), ms_1), (mi_2.clone(), ms_2)],
mip_stats_cfg.clone(),
))
.unwrap();
match is_consistent(&store, shutdown_start, shutdown_end) {
Err(IsConsistentWithShutdownPeriodError::NonConsistent(mi, ..)) => {
assert_eq!(mi, mi_1);
}
_ => panic!("is_consistent expects a non consistent error"),
}
update_store(&mut store, shutdown_start, shutdown_end);
assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
store.update_network_version_stats(
get_slot_ts(shutdown_end.get_next_slot(THREAD_COUNT).unwrap()),
Some((1, None)),
);
let (first_mi_info, first_mi_state) = store.store.first_key_value().unwrap();
assert_eq!(*first_mi_info.name, mi_1.name);
assert_eq!(
ComponentStateTypeId::from(&first_mi_state.state),
ComponentStateTypeId::Started
);
let (last_mi_info, last_mi_state) = store.store.last_key_value().unwrap();
assert_eq!(*last_mi_info.name, mi_2.name);
assert_eq!(
ComponentStateTypeId::from(&last_mi_state.state),
ComponentStateTypeId::Defined
);
}
}
#[test]
fn test_mip_store_db() {
let genesis_timestamp = MassaTime::from_millis(0);
let get_slot_ts =
|slot| get_block_slot_timestamp(THREAD_COUNT, T0, genesis_timestamp, slot).unwrap();
let temp_dir = tempdir().expect("Unable to create a temp folder");
let db_config = MassaDBConfig {
path: temp_dir.path().to_path_buf(),
max_history_length: 100,
max_final_state_elements_size: 100_000,
max_versioning_elements_size: 100_000,
thread_count: THREAD_COUNT,
max_ledger_backups: 10,
};
let db = Arc::new(RwLock::new(
Box::new(MassaDB::new(db_config)) as Box<(dyn MassaDBController + 'static)>
));
let mi_1 = MipInfo {
name: "MIP-0002".to_string(),
version: 2,
components: BTreeMap::from([(MipComponent::Address, 1)]),
start: get_slot_ts(Slot::new(2, 0)),
timeout: get_slot_ts(Slot::new(3, 0)),
activation_delay: MassaTime::from_millis(10),
};
let ms_1 = advance_state_until(ComponentState::defined(), &mi_1);
let mi_2 = MipInfo {
name: "MIP-0003".to_string(),
version: 3,
components: BTreeMap::from([(MipComponent::Address, 2)]),
start: get_slot_ts(Slot::new(4, 2)),
timeout: get_slot_ts(Slot::new(7, 2)),
activation_delay: MassaTime::from_millis(10),
};
let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
let mip_stats_config = MipStatsConfig {
block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
warn_announced_version_ratio: Ratio::new_raw(30, 100),
};
let mut mip_store = MipStore::try_from((
[(mi_1.clone(), ms_1.clone()), (mi_2.clone(), ms_2.clone())],
mip_stats_config.clone(),
))
.expect("Cannot create an empty MIP store");
mip_store.extend_from_db(db.clone()).unwrap();
assert_eq!(mip_store.0.read().store.len(), 2);
assert_eq!(
mip_store.0.read().store.first_key_value(),
Some((&mi_1, &ms_1))
);
assert_eq!(
mip_store.0.read().store.last_key_value(),
Some((&mi_2, &ms_2))
);
let active_at = get_slot_ts(Slot::new(2, 5));
let ms_1_ = advance_state_until(ComponentState::active(active_at), &mi_1);
let mip_store_ =
MipStore::try_from(([(mi_1.clone(), ms_1_.clone())], mip_stats_config.clone()))
.expect("Cannot create an empty MIP store");
let (updated, added) = mip_store.update_with(&mip_store_).unwrap();
assert_eq!(updated.len(), 1);
assert_eq!(added.len(), 0);
assert_eq!(mip_store.0.read().store.len(), 2);
assert_eq!(
mip_store.0.read().store.first_key_value(),
Some((&mi_1, &ms_1_))
);
assert_eq!(
mip_store.0.read().store.last_key_value(),
Some((&mi_2, &ms_2))
);
let mut db_batch = DBBatch::new();
let mut db_versioning_batch = DBBatch::new();
let slot_bounds_ = (&Slot::new(1, 0), &Slot::new(4, 2));
let between = (&get_slot_ts(*slot_bounds_.0), &get_slot_ts(*slot_bounds_.1));
mip_store
.update_batches(&mut db_batch, &mut db_versioning_batch, Some(between))
.unwrap();
assert_eq!(db_batch.len(), 1); assert_eq!(db_versioning_batch.len(), 3); let mut guard_db = db.write();
guard_db.write_batch(db_batch, db_versioning_batch, Some(Slot::new(3, 0)));
drop(guard_db);
let mut mip_store_2 = MipStore::try_from((
[(mi_1.clone(), ms_1.clone()), (mi_2.clone(), ms_2.clone())],
mip_stats_config.clone(),
))
.expect("Cannot create an empty MIP store");
mip_store_2.extend_from_db(db.clone()).unwrap();
let guard_1 = mip_store.0.read();
let guard_2 = mip_store_2.0.read();
let st1_raw = guard_1.deref();
let st2_raw = guard_2.deref();
assert_eq!(st1_raw, st2_raw);
let mut count = 0;
for (ser_key, ser_value) in db.read().iterator_cf(STATE_CF, MassaIteratorMode::Start) {
assert!(mip_store.is_key_value_valid(&ser_key, &ser_value));
count += 1;
}
assert_gt!(count, 0);
let mut count2 = 0;
for (ser_key, ser_value) in db
.read()
.iterator_cf(VERSIONING_CF, MassaIteratorMode::Start)
{
assert!(mip_store.is_key_value_valid(&ser_key, &ser_value));
count2 += 1;
}
assert_gt!(count2, 0);
}
#[test]
fn test_mip_store_stats() {
let genesis_timestamp = MassaTime::from_millis(0);
let get_slot_ts =
|slot| get_block_slot_timestamp(THREAD_COUNT, T0, genesis_timestamp, slot).unwrap();
let mip_stats_config = MipStatsConfig {
block_count_considered: 2,
warn_announced_version_ratio: Ratio::new_raw(30, 100),
};
let activation_delay = MassaTime::from_millis(100);
let timeout = MassaTime::now().saturating_add(MassaTime::from_millis(50_000)); let mi_1 = MipInfo {
name: "MIP-0001".to_string(),
version: 1,
components: BTreeMap::from([(MipComponent::Address, 1)]),
start: MassaTime::from_millis(2),
timeout,
activation_delay,
};
let ms_1 = advance_state_until(ComponentState::started(Ratio::zero()), &mi_1);
let mut mip_store =
MipStoreRaw::try_from(([(mi_1.clone(), ms_1)], mip_stats_config)).unwrap();
mip_store.update_network_version_stats(get_slot_ts(Slot::new(1, 0)), Some((0, Some(1))));
assert_eq!(mip_store.stats.network_version_counters.len(), 1);
assert_eq!(mip_store.stats.network_version_counters.get(&1), Some(&1));
mip_store.update_network_version_stats(get_slot_ts(Slot::new(1, 0)), Some((0, Some(1))));
assert_eq!(mip_store.stats.network_version_counters.len(), 1);
assert_eq!(mip_store.stats.network_version_counters.get(&1), Some(&2));
let (mi_, ms_) = mip_store.store.last_key_value().unwrap();
assert_eq!(*mi_, mi_1);
assert_matches!(ms_.state, ComponentState::LockedIn(..));
let mut at = MassaTime::now();
at = at.saturating_add(activation_delay);
assert_eq!(
ms_.state_at(at, mi_1.start, mi_1.timeout, mi_1.activation_delay),
Ok(ComponentStateTypeId::Active)
);
mip_store.update_network_version_stats(get_slot_ts(Slot::new(1, 0)), Some((1, Some(2))));
assert_eq!(mip_store.stats.network_version_counters.len(), 2);
assert_eq!(mip_store.stats.network_version_counters.get(&1), Some(&1));
assert_eq!(mip_store.stats.network_version_counters.get(&2), Some(&1));
}
}