massa_versioning/
versioning.rs

1use std::cmp::Ordering;
2use std::collections::hash_map::Entry;
3use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
4use std::iter;
5use std::ops::Deref;
6use std::sync::Arc;
7
8use machine::{machine, transitions};
9use num::{rational::Ratio, Zero};
10use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive};
11use parking_lot::RwLock;
12use thiserror::Error;
13use tracing::{debug, warn};
14
15use massa_db_exports::{
16    DBBatch, ShareableMassaDBController, MIP_STORE_PREFIX, MIP_STORE_STATS_PREFIX, STATE_CF,
17    VERSIONING_CF,
18};
19use massa_models::config::MIP_STORE_STATS_BLOCK_CONSIDERED;
20#[allow(unused_imports)]
21use massa_models::config::VERSIONING_ACTIVATION_DELAY_MIN;
22use massa_models::config::VERSIONING_THRESHOLD_TRANSITION_ACCEPTED;
23use massa_models::error::ModelsError;
24use massa_models::slot::Slot;
25use massa_models::timeslots::get_block_slot_timestamp;
26use massa_serialization::{DeserializeError, Deserializer, SerializeError, Serializer};
27use massa_time::MassaTime;
28use variant_count::VariantCount;
29
30use crate::versioning_ser_der::{
31    MipInfoDeserializer, MipInfoSerializer, MipStateDeserializer, MipStateSerializer,
32    MipStoreStatsDeserializer, MipStoreStatsSerializer,
33};
34
35/// Versioning component enum
36#[allow(missing_docs)]
37#[derive(
38    Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, FromPrimitive, IntoPrimitive, VariantCount,
39)]
40#[repr(u32)]
41pub enum MipComponent {
42    // Address and KeyPair versions are directly related
43    Address,
44    KeyPair,
45    Block,
46    VM,
47    FinalStateHashKind,
48    Execution,
49    FinalState,
50    #[doc(hidden)]
51    #[num_enum(default)]
52    __Nonexhaustive,
53}
54
55/// MIP info (name & versions & time range for a MIP)
56#[derive(Clone, Debug)]
57pub struct MipInfo {
58    /// MIP name or descriptive name
59    pub name: String,
60    /// Network (or global) version (to be included in block header)
61    pub version: u32,
62    /// Components concerned by this versioning (e.g. a new Block version), and the associated component_version
63    pub components: BTreeMap<MipComponent, u32>,
64    /// a timestamp at which the version gains its meaning (e.g. announced in block header)
65    pub start: MassaTime,
66    /// a timestamp at the which the deployment is considered failed
67    pub timeout: MassaTime,
68    /// Once deployment has been locked, wait for this duration before deployment is considered active
69    pub activation_delay: MassaTime,
70}
71
72// Need Ord / PartialOrd so it is properly sorted in BTreeMap
73
74impl Ord for MipInfo {
75    fn cmp(&self, other: &Self) -> Ordering {
76        (
77            self.start,
78            self.timeout,
79            self.activation_delay,
80            &self.name,
81            &self.version,
82            &self.components,
83        )
84            .cmp(&(
85                other.start,
86                other.timeout,
87                other.activation_delay,
88                &other.name,
89                &other.version,
90                &other.components,
91            ))
92    }
93}
94
95impl PartialOrd for MipInfo {
96    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
97        Some(self.cmp(other))
98    }
99}
100
101impl PartialEq for MipInfo {
102    fn eq(&self, other: &Self) -> bool {
103        self.name == other.name
104            && self.version == other.version
105            && self.components == other.components
106            && self.start == other.start
107            && self.timeout == other.timeout
108            && self.activation_delay == other.activation_delay
109    }
110}
111
112impl Eq for MipInfo {}
113
114machine!(
115    /// State machine for a Versioning component that tracks the deployment state
116    #[derive(Clone, Copy, Debug, PartialEq)]
117    pub(crate) enum ComponentState {
118        /// Initial state
119        Defined,
120        /// Past start, can only go to LockedIn after the threshold is above a given value
121        Started { pub(crate) vote_ratio: Ratio<u64> },
122        /// Locked but wait for some time before going to active (to let users the time to upgrade)
123        /// 'at' is the timestamp where the transition from 'Started' to 'LockedIn' happened
124        LockedIn { pub(crate) at: MassaTime },
125        /// After LockedIn, deployment is considered successful (after activation delay)
126        /// 'at' is the timestamp where the transition from 'LockedIn' to 'Active' happened
127        Active { pub(crate) at: MassaTime },
128        /// Past the timeout, if LockedIn is not reach
129        Failed,
130    }
131);
132
133impl Default for ComponentState {
134    fn default() -> Self {
135        Self::Defined(Defined {})
136    }
137}
138
139impl ComponentState {
140    fn is_final(&self) -> bool {
141        matches!(
142            self,
143            ComponentState::Active(..) | ComponentState::Failed(..) | ComponentState::Error
144        )
145    }
146}
147
148#[allow(missing_docs)]
149#[derive(
150    IntoPrimitive, Debug, Clone, Eq, PartialEq, TryFromPrimitive, PartialOrd, Ord, VariantCount,
151)]
152#[repr(u32)]
153pub enum ComponentStateTypeId {
154    Error = 0,
155    Defined = 1,
156    Started = 2,
157    LockedIn = 3,
158    Active = 4,
159    Failed = 5,
160}
161
162impl From<&ComponentState> for ComponentStateTypeId {
163    fn from(value: &ComponentState) -> Self {
164        match value {
165            ComponentState::Error => ComponentStateTypeId::Error,
166            ComponentState::Defined(_) => ComponentStateTypeId::Defined,
167            ComponentState::Started(_) => ComponentStateTypeId::Started,
168            ComponentState::LockedIn(_) => ComponentStateTypeId::LockedIn,
169            ComponentState::Active(_) => ComponentStateTypeId::Active,
170            ComponentState::Failed(_) => ComponentStateTypeId::Failed,
171        }
172    }
173}
174
175/// A message to update the `ComponentState`
176#[derive(Clone, Debug)]
177pub struct Advance {
178    /// from MipInfo.start
179    pub start_timestamp: MassaTime,
180    /// from MipInfo.timeout
181    pub timeout: MassaTime,
182    /// from MipInfo.activation_delay
183    pub activation_delay: MassaTime,
184
185    /// % of past blocks with this version
186    pub threshold: Ratio<u64>,
187    /// Current time (timestamp)
188    pub now: MassaTime,
189}
190
191impl PartialEq for Advance {
192    fn eq(&self, other: &Self) -> bool {
193        self.start_timestamp == other.start_timestamp
194            && self.timeout == other.timeout
195            && self.threshold == other.threshold
196            && self.now == other.now
197            && self.activation_delay == other.activation_delay
198    }
199}
200
201impl Eq for Advance {}
202
203transitions!(ComponentState,
204    [
205        (Defined, Advance) => [Defined, Started, Failed],
206        (Started, Advance) => [Started, LockedIn, Failed],
207        (LockedIn, Advance) => [LockedIn, Active],
208        (Active, Advance) => Active,
209        (Failed, Advance) => Failed
210    ]
211);
212
213impl Defined {
214    /// Update state from state Defined
215    pub fn on_advance(self, input: Advance) -> ComponentState {
216        match input.now {
217            n if n >= input.timeout => ComponentState::failed(),
218            n if n >= input.start_timestamp => ComponentState::started(Ratio::zero()),
219            _ => ComponentState::Defined(Defined {}),
220        }
221    }
222}
223
224impl Started {
225    /// Update state from state Started
226    pub fn on_advance(self, input: Advance) -> ComponentState {
227        if input.now >= input.timeout {
228            return ComponentState::failed();
229        }
230
231        if input.threshold >= VERSIONING_THRESHOLD_TRANSITION_ACCEPTED {
232            debug!("(VERSIONING LOG) transition accepted, locking in");
233            ComponentState::locked_in(input.now)
234        } else {
235            ComponentState::started(input.threshold)
236        }
237    }
238}
239
240impl LockedIn {
241    /// Update state from state LockedIn ...
242    pub fn on_advance(self, input: Advance) -> ComponentState {
243        if input.now > self.at.saturating_add(input.activation_delay) {
244            debug!("(VERSIONING LOG) locked version has become active");
245            ComponentState::active(input.now)
246        } else {
247            ComponentState::locked_in(self.at)
248        }
249    }
250}
251
252impl Active {
253    /// Update state (will always stay in state Active)
254    pub fn on_advance(self, _input: Advance) -> Active {
255        Active { at: self.at }
256    }
257}
258
259impl Failed {
260    /// Update state (will always stay in state Failed)
261    pub fn on_advance(self, _input: Advance) -> Failed {
262        Failed {}
263    }
264}
265
266// A Lightweight version of 'Advance' (used in MipState history)
267#[derive(Clone, Debug)]
268pub struct AdvanceLW {
269    /// % of past blocks with this version
270    pub threshold: Ratio<u64>,
271    /// Current time (timestamp)
272    pub now: MassaTime,
273}
274
275impl From<&Advance> for AdvanceLW {
276    fn from(value: &Advance) -> Self {
277        Self {
278            threshold: value.threshold,
279            now: value.now,
280        }
281    }
282}
283
284impl Ord for AdvanceLW {
285    fn cmp(&self, other: &Self) -> Ordering {
286        (self.now, self.threshold).cmp(&(other.now, other.threshold))
287    }
288}
289
290impl PartialOrd for AdvanceLW {
291    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
292        Some(self.cmp(other))
293    }
294}
295
296impl PartialEq for AdvanceLW {
297    fn eq(&self, other: &Self) -> bool {
298        self.threshold == other.threshold && self.now == other.now
299    }
300}
301
302impl Eq for AdvanceLW {}
303
304/// Error returned by `MipState::is_consistent_with`
305#[derive(Error, Debug, PartialEq)]
306pub enum IsConsistentError {
307    // State is not consistent with associated MipInfo, ex: State is active but MipInfo.start was not reach yet
308    #[error("MipState history is empty")]
309    EmptyHistory,
310    #[error("MipState is at state Error")]
311    AtError,
312    #[error("History must start at state 'Defined' and not {0:?}")]
313    InvalidHistory(ComponentStateTypeId),
314    #[error("Non consistent state: {0:?} versus rebuilt state: {1:?}")]
315    NonConsistent(ComponentState, ComponentState),
316    #[error("Invalid data in MIP info, start >= timeout")]
317    Invalid,
318}
319
320/// Wrapper of ComponentState (in order to keep state history)
321#[derive(Debug, Clone, PartialEq)]
322pub struct MipState {
323    pub(crate) state: ComponentState,
324    pub(crate) history: BTreeMap<AdvanceLW, ComponentStateTypeId>,
325}
326
327impl MipState {
328    /// Create
329    pub fn new(defined: MassaTime) -> Self {
330        let state: ComponentState = Default::default(); // Default is Defined
331        let state_id = ComponentStateTypeId::from(&state);
332        // Build a 'dummy' advance lw msg for state Defined, this is to avoid using an
333        // Option<AdvanceLW> in MipStateHistory::history
334        let advance = AdvanceLW {
335            threshold: Default::default(),
336            now: defined,
337            // activation_delay: MassaTime::from_millis(0),
338        };
339
340        let history = BTreeMap::from([(advance, state_id)]);
341        Self { state, history }
342    }
343
344    /// Create a new state from an existing state - resulting state will be at state "Defined"
345    pub fn reset_from(&self) -> Option<Self> {
346        match self.history.first_key_value() {
347            Some((advance, state_id)) if *state_id == ComponentStateTypeId::Defined => {
348                Some(MipState::new(advance.now))
349            }
350            _ => None,
351        }
352    }
353
354    /// Advance the state
355    /// Can be called as multiple times as it will only store what changes the state in history
356    pub fn on_advance(&mut self, input: &Advance) {
357        // Check that input.now is after last item in history
358        // We don't want to go backward
359        let is_forward = self
360            .history
361            .last_key_value()
362            .map(|(adv, _)| adv.now < input.now)
363            .unwrap_or(false);
364
365        if is_forward {
366            // machines crate (for state machine) does not support passing ref :-/
367            let state = self.state.on_advance(input.clone());
368            // Update history as well
369            if state != self.state {
370                // Avoid storing too much things in history
371                // Here we avoid storing for every threshold update
372                if !(matches!(state, ComponentState::Started(Started { .. }))
373                    && matches!(self.state, ComponentState::Started(Started { .. })))
374                {
375                    self.history
376                        .insert(input.into(), ComponentStateTypeId::from(&state));
377                }
378                self.state = state;
379            }
380        }
381    }
382
383    /// Given a corresponding MipInfo, check if state is consistent
384    /// it is consistent
385    ///   if state can be at this position (e.g. can it be at state "Started" according to given time range)
386    ///   if history is consistent with current state
387    /// Return false for state == ComponentState::Error
388    pub fn is_consistent_with(&self, mip_info: &MipInfo) -> Result<(), IsConsistentError> {
389        // Always return false for state Error or if history is empty
390        if matches!(&self.state, &ComponentState::Error) {
391            return Err(IsConsistentError::AtError);
392        }
393
394        if mip_info.start >= mip_info.timeout {
395            return Err(IsConsistentError::Invalid);
396        }
397
398        if self.history.is_empty() {
399            return Err(IsConsistentError::EmptyHistory);
400        }
401
402        // safe to unwrap (already tested if empty or not)
403        let (initial_ts, initial_state_id) = self.history.first_key_value().unwrap();
404        if *initial_state_id != ComponentStateTypeId::Defined {
405            // self.history does not start with Defined -> (always) false
406            return Err(IsConsistentError::InvalidHistory(initial_state_id.clone()));
407        }
408
409        if mip_info.start < initial_ts.now || mip_info.timeout < initial_ts.now {
410            // MIP info start (or timeout) is before Defined timestamp??
411            return Err(IsConsistentError::InvalidHistory(initial_state_id.clone()));
412        }
413
414        // Build a new MipStateHistory from initial state, replaying the whole history
415        // but with given versioning info then compare
416        let mut vsh = MipState::new(initial_ts.now);
417        let mut advance_msg = Advance {
418            start_timestamp: mip_info.start,
419            timeout: mip_info.timeout,
420            threshold: Ratio::zero(),
421            now: initial_ts.now,
422            activation_delay: mip_info.activation_delay,
423        };
424
425        for (adv, _state) in self.history.iter().skip(1) {
426            advance_msg.now = adv.now;
427            advance_msg.threshold = adv.threshold;
428            vsh.on_advance(&advance_msg);
429        }
430
431        // Advance state if both are at 'Started' (to have the same threshold)
432        // Note: because in history we do not add entries for every threshold update
433        if let (
434            ComponentState::Started(Started {
435                vote_ratio: threshold,
436            }),
437            ComponentState::Started(Started {
438                vote_ratio: threshold_2,
439            }),
440        ) = (vsh.state, self.state)
441        {
442            if threshold_2 != threshold {
443                advance_msg.threshold = threshold_2;
444                // Need to advance now timestamp otherwise it will be ignored
445                advance_msg.now = advance_msg.now.saturating_add(MassaTime::from_millis(1));
446                vsh.on_advance(&advance_msg);
447            }
448        }
449
450        if vsh == *self {
451            Ok(())
452        } else {
453            Err(IsConsistentError::NonConsistent(self.state, vsh.state))
454        }
455    }
456
457    /// Query state at given timestamp
458    /// TODO: add doc for start & timeout parameter? why do we need them?
459    pub fn state_at(
460        &self,
461        ts: MassaTime,
462        start: MassaTime,
463        timeout: MassaTime,
464        activation_delay: MassaTime,
465    ) -> Result<ComponentStateTypeId, StateAtError> {
466        if self.history.is_empty() {
467            return Err(StateAtError::EmptyHistory);
468        }
469
470        // Optim: this avoids iterating over history (cheap to retrieve first item)
471        let first = self.history.first_key_value().unwrap(); // safe to unwrap
472        if ts < first.0.now {
473            // Before initial state
474            return Err(StateAtError::BeforeInitialState(first.1.clone(), ts));
475        }
476
477        // At this point, we are >= the first state in history
478        let mut lower_bound = None;
479        let mut higher_bound = None;
480        let mut is_after_last = false;
481
482        // Optim: this avoids iterating over history (cheap to retrieve first item)
483        let last = self.history.last_key_value().unwrap(); // safe to unwrap
484        if ts > last.0.now {
485            lower_bound = Some(last);
486            is_after_last = true;
487        }
488
489        if !is_after_last {
490            // We are in between two states in history, find bounds
491            for (adv, state_id) in self.history.iter() {
492                if adv.now <= ts {
493                    lower_bound = Some((adv, state_id));
494                }
495                if adv.now >= ts && higher_bound.is_none() {
496                    higher_bound = Some((adv, state_id));
497                    break;
498                }
499            }
500        }
501
502        match (lower_bound, higher_bound) {
503            (Some((_adv_1, st_id_1)), Some((_adv_2, _st_id_2))) => {
504                // Between 2 states (e.g. between Defined && Started) -> return Defined
505                Ok(st_id_1.clone())
506            }
507            (Some((adv, st_id)), None) => {
508                // After the last state in history -> need to advance the state and return
509                let threshold_for_transition = VERSIONING_THRESHOLD_TRANSITION_ACCEPTED;
510                // Note: Please update this if MipState transitions change as it might not hold true
511                if *st_id == ComponentStateTypeId::Started
512                    && adv.threshold < threshold_for_transition
513                    && ts < timeout
514                // adv.timeout - TODO: test this
515                {
516                    Err(StateAtError::Unpredictable)
517                } else {
518                    let msg = Advance {
519                        start_timestamp: start,
520                        timeout,
521                        threshold: adv.threshold,
522                        now: ts,
523                        // activation_delay: adv.activation_delay,
524                        activation_delay,
525                    };
526                    // Return the resulting state after advance
527                    let state = self.state.on_advance(msg);
528                    Ok(ComponentStateTypeId::from(&state))
529                }
530            }
531            _ => {
532                // 1. Before the first state in history: already covered
533                // 2. None, None: already covered - empty history
534                Err(StateAtError::EmptyHistory)
535            }
536        }
537    }
538
539    /// Return the time when state will go from LockedIn to Active, None if not already LockedIn
540    pub fn activation_at(&self, mip_info: &MipInfo) -> Option<MassaTime> {
541        match self.state {
542            ComponentState::LockedIn(LockedIn { at }) => {
543                Some(at.saturating_add(mip_info.activation_delay))
544            }
545            _ => None,
546        }
547    }
548
549    /// Return True if state can not change anymore (e.g. Active, Failed or Error)
550    pub fn is_final(&self) -> bool {
551        self.state.is_final()
552    }
553}
554
555/// Error returned by MipStateHistory::state_at
556#[allow(missing_docs)]
557#[derive(Error, Debug, PartialEq)]
558pub enum StateAtError {
559    #[error("Initial state ({0:?}) only defined after timestamp: {1}")]
560    BeforeInitialState(ComponentStateTypeId, MassaTime),
561    #[error("Empty history, should never happen")]
562    EmptyHistory,
563    #[error("Cannot predict value: threshold not reached yet")]
564    Unpredictable,
565}
566
567// Store
568
569/// Database for all MIP info
570#[derive(Debug, Clone)]
571pub struct MipStore(pub Arc<RwLock<MipStoreRaw>>);
572
573impl MipStore {
574    /// Retrieve the current network version to set in block header
575    pub fn get_network_version_current(&self) -> u32 {
576        let lock = self.0.read();
577        let store = lock.deref();
578        // Current version == last active
579        store
580            .store
581            .iter()
582            .rev()
583            .find_map(|(k, v)| (matches!(v.state, ComponentState::Active(_))).then_some(k.version))
584            .unwrap_or(0)
585    }
586
587    /// Retrieve the last active version at the given timestamp
588    pub fn get_network_version_active_at(&self, ts: MassaTime) -> u32 {
589        let lock = self.0.read();
590        let store = lock.deref();
591        store
592            .store
593            .iter()
594            .rev()
595            .find_map(|(k, v)| match v.state {
596                ComponentState::Active(Active { at }) if at <= ts => Some(k.version),
597                _ => None,
598            })
599            .unwrap_or(0)
600    }
601
602    /// Retrieve the network version number to announce in block header
603    /// return 0 is there is nothing to announce
604    pub fn get_network_version_to_announce(&self) -> Option<u32> {
605        let lock = self.0.read();
606        let store = lock.deref();
607        // Announce the latest versioning info in Started / LockedIn state
608        // Defined == Not yet ready to announce
609        // Active == current version
610        store.store.iter().rev().find_map(|(k, v)| {
611            matches!(
612                &v.state,
613                &ComponentState::Started(_) | &ComponentState::LockedIn(_)
614            )
615            .then_some(k.version)
616        })
617    }
618
619    pub fn update_network_version_stats(
620        &mut self,
621        slot_timestamp: MassaTime,
622        network_versions: Option<(u32, Option<u32>)>,
623    ) {
624        let mut lock = self.0.write();
625        lock.update_network_version_stats(slot_timestamp, network_versions);
626    }
627
628    #[allow(clippy::result_large_err)]
629    pub fn update_with(
630        &mut self,
631        mip_store: &MipStore,
632    ) -> Result<(Vec<MipInfo>, BTreeMap<MipInfo, MipState>), UpdateWithError> {
633        let mut lock = self.0.write();
634        let lock_other = mip_store.0.read();
635        lock.update_with(lock_other.deref())
636    }
637
638    // Query
639
640    /// Get latest version at given timestamp (e.g. slot) for the given MipComponent
641    pub fn get_latest_component_version_at(&self, component: &MipComponent, ts: MassaTime) -> u32 {
642        let guard = self.0.read();
643        guard.get_latest_component_version_at(component, ts)
644    }
645
646    /// Get all versions in 'Active state' for the given MipComponent
647    pub(crate) fn get_all_active_component_versions(&self, component: &MipComponent) -> Vec<u32> {
648        let guard = self.0.read();
649        guard.get_all_active_component_versions(component)
650    }
651
652    /// Get all versions (at any state) for the given MipComponent
653    pub(crate) fn get_all_component_versions(
654        &self,
655        component: &MipComponent,
656    ) -> BTreeMap<u32, ComponentStateTypeId> {
657        let guard = self.0.read();
658        guard.get_all_component_versions(component)
659    }
660
661    // GRPC
662
663    /// Retrieve a list of MIP info with their corresponding state (as id) - used for grpc API
664    pub fn get_mip_status(&self) -> BTreeMap<MipInfo, ComponentStateTypeId> {
665        let guard = self.0.read();
666        guard
667            .store
668            .iter()
669            .map(|(mip_info, mip_state)| {
670                (
671                    mip_info.clone(),
672                    ComponentStateTypeId::from(&mip_state.state),
673                )
674            })
675            .collect()
676    }
677
678    // Network restart
679    pub fn is_consistent_with_shutdown_period(
680        &self,
681        shutdown_start: Slot,
682        shutdown_end: Slot,
683        thread_count: u8,
684        t0: MassaTime,
685        genesis_timestamp: MassaTime,
686    ) -> Result<(), IsConsistentWithShutdownPeriodError> {
687        let guard = self.0.read();
688        guard.is_consistent_with_shutdown_period(
689            shutdown_start,
690            shutdown_end,
691            thread_count,
692            t0,
693            genesis_timestamp,
694        )
695    }
696
697    #[allow(dead_code)]
698    pub fn update_for_network_shutdown(
699        &mut self,
700        shutdown_start: Slot,
701        shutdown_end: Slot,
702        thread_count: u8,
703        t0: MassaTime,
704        genesis_timestamp: MassaTime,
705    ) -> Result<(), ModelsError> {
706        let mut guard = self.0.write();
707        guard.update_for_network_shutdown(
708            shutdown_start,
709            shutdown_end,
710            thread_count,
711            t0,
712            genesis_timestamp,
713        )
714    }
715
716    pub fn is_key_value_valid(&self, serialized_key: &[u8], serialized_value: &[u8]) -> bool {
717        let guard = self.0.read();
718        guard.is_key_value_valid(serialized_key, serialized_value)
719    }
720
721    // DB
722
723    pub fn update_batches(
724        &self,
725        db_batch: &mut DBBatch,
726        db_versioning_batch: &mut DBBatch,
727        between: Option<(&MassaTime, &MassaTime)>,
728    ) -> Result<(), SerializeError> {
729        let guard = self.0.read();
730        guard.update_batches(db_batch, db_versioning_batch, between)
731    }
732
733    pub fn extend_from_db(
734        &mut self,
735        db: ShareableMassaDBController,
736    ) -> Result<(Vec<MipInfo>, BTreeMap<MipInfo, MipState>), ExtendFromDbError> {
737        let mut guard = self.0.write();
738        guard.extend_from_db(db)
739    }
740
741    /// MIPs of this store that a bootstrap server's streamed state (`db`) does not contain, and
742    /// whose `start` is at or before `at`, the timestamp of the bootstrapped final slot.
743    ///
744    /// Such a server runs a release that predates the MIP, so it cannot provide the MIP's state.
745    /// Accepting it would leave this node replaying the MIP from its compiled-in `Defined` state,
746    /// starting at its own first processed slot: the history it records (and, past lock-in, its
747    /// activation) would differ from the rest of the network's, and once the MIP is Active that
748    /// history is part of the hashed final state. Before `start` nobody has recorded anything for
749    /// the MIP yet, so replaying it locally is deterministic and such a server is still usable.
750    pub fn started_mips_missing_from_db(
751        &self,
752        db: &ShareableMassaDBController,
753        at: MassaTime,
754    ) -> Vec<MipInfo> {
755        let guard = self.0.read();
756        guard.started_mips_missing_from_db(db, at)
757    }
758
759    pub fn reset_db(&self, db: ShareableMassaDBController) {
760        {
761            let mut guard = db.write();
762            guard.delete_prefix(MIP_STORE_PREFIX, STATE_CF, None);
763            guard.delete_prefix(MIP_STORE_PREFIX, VERSIONING_CF, None);
764            guard.delete_prefix(MIP_STORE_STATS_PREFIX, VERSIONING_CF, None);
765        }
766    }
767
768    /// Create a MIP store with what is written on the disk
769    pub fn try_from_db(
770        db: ShareableMassaDBController,
771        cfg: MipStatsConfig,
772    ) -> Result<Self, ExtendFromDbError> {
773        MipStoreRaw::try_from_db(db, cfg).map(|store_raw| Self(Arc::new(RwLock::new(store_raw))))
774    }
775
776    // debug
777    // pub fn len(&self) -> usize {
778    //     let guard = self.0.read();
779    //     guard.store.len()
780    // }
781}
782
783impl<const N: usize> TryFrom<([(MipInfo, MipState); N], MipStatsConfig)> for MipStore {
784    type Error = UpdateWithError;
785
786    fn try_from(
787        (value, cfg): ([(MipInfo, MipState); N], MipStatsConfig),
788    ) -> Result<Self, Self::Error> {
789        MipStoreRaw::try_from((value, cfg)).map(|store_raw| Self(Arc::new(RwLock::new(store_raw))))
790    }
791}
792
793/// Statistics in MipStoreRaw
794#[derive(Debug, Clone, PartialEq)]
795pub struct MipStatsConfig {
796    pub block_count_considered: usize,
797    pub warn_announced_version_ratio: Ratio<u64>,
798}
799
800/// In order for a MIP to be accepted, we compute statistics about other node 'network' version announcement
801#[derive(Debug, Clone, PartialEq)]
802pub(crate) struct MipStoreStats {
803    // config for block count to consider when computing the vote ratio
804    pub(crate) config: MipStatsConfig,
805    // Last network version announcements (in last block header)
806    // Used to clean up the field: network_version_counters (pop the oldest then subtract matching counter)
807    pub(crate) latest_announcements: VecDeque<u32>,
808    // A map where key: network version, value: announcement for this network version count
809    // Note: to avoid various attacks, we have as many counters as version announcements
810    //       + if a counter reset to 0, it is removed from the hash map
811    pub(crate) network_version_counters: HashMap<u32, u64>,
812}
813
814impl MipStoreStats {
815    pub(crate) fn new(config: MipStatsConfig) -> Self {
816        Self {
817            config: config.clone(),
818            latest_announcements: VecDeque::with_capacity(config.block_count_considered),
819            network_version_counters: HashMap::with_capacity(config.block_count_considered),
820        }
821    }
822
823    // reset stats - used in `update_for_network_shutdown` function
824    fn reset(&mut self) {
825        self.latest_announcements.clear();
826        self.network_version_counters.clear();
827    }
828}
829
830/// Error returned by `MipStoreRaw::update_with`
831#[derive(Error, Debug, PartialEq)]
832pub enum UpdateWithError {
833    // State is not consistent with associated MipInfo, ex: State is active but MipInfo.start was not reach yet
834    #[error("MipInfo {0:#?} is not consistent with state: {1:#?}, error: {2}")]
835    NonConsistent(MipInfo, MipState, IsConsistentError),
836    // ex: State is already started but received state is only defined
837    #[error("For MipInfo {0:?}, trying to downgrade from state {1:?} to {2:?}")]
838    Downgrade(MipInfo, ComponentState, ComponentState),
839    // ex: MipInfo 2 start is before MipInfo 1 timeout (MipInfo timings should only be sequential)
840    #[error("MipInfo {0:?} has overlapping data of MipInfo {1:?}")]
841    Overlapping(MipInfo, MipInfo),
842    #[error("MipInfo {0:?} has an invalid activation delay value: {1}, min allowed: {2}")]
843    InvalidActivationDelay(MipInfo, MassaTime, MassaTime),
844}
845
846/// Error returned by 'extend_from_db`
847#[derive(Error, Debug)]
848pub enum ExtendFromDbError {
849    #[error("Unable to get an handle over db column: {0}")]
850    UnknownDbColumn(String),
851    #[error("{0}")]
852    Update(#[from] UpdateWithError),
853    #[error("{0}")]
854    Deserialize(String),
855}
856
857/// Error returned by 'is_consistent_with_shutdown_period`
858#[derive(Error, Debug)]
859pub enum IsConsistentWithShutdownPeriodError {
860    #[error("{0}")]
861    Update(#[from] ModelsError),
862    #[error("MipInfo: {0:?} (state: {1:?}) is not consistent with shutdown: {2} {3}")]
863    NonConsistent(MipInfo, ComponentState, MassaTime, MassaTime),
864}
865
866#[derive(Error, Debug)]
867pub enum IsKVValidError {
868    #[error("{0}")]
869    Deserialize(String),
870    #[error("Invalid prefix for key")]
871    InvalidPrefix,
872}
873
874/// Store of all versioning info
875#[derive(Debug, Clone, PartialEq)]
876pub struct MipStoreRaw {
877    pub(crate) store: BTreeMap<MipInfo, MipState>,
878    pub(crate) stats: MipStoreStats,
879}
880
881impl MipStoreRaw {
882    /// Update our store with another (usually after a bootstrap where we received another store)
883    /// Return list of updated / added if successful, UpdateWithError otherwise
884    #[allow(clippy::result_large_err)]
885    pub fn update_with(
886        &mut self,
887        store_raw: &MipStoreRaw,
888    ) -> Result<(Vec<MipInfo>, BTreeMap<MipInfo, MipState>), UpdateWithError> {
889        // iter over items in given store:
890        // -> 2 cases:
891        // * MipInfo is already in self store -> add to 'to_update' list
892        // * MipInfo is not in self.store -> We received a new MipInfo so we are running an out dated version
893        //                                   of the software
894        //                                   We then return the list of new MipInfo so we can warn and ask
895        //                                   to update the software
896
897        let mut component_versions: HashMap<MipComponent, u32> = self
898            .store
899            .iter()
900            .flat_map(|c| {
901                c.0.components
902                    .iter()
903                    .map(|(mip_component, component_version)| {
904                        (mip_component.clone(), *component_version)
905                    })
906            })
907            .collect();
908        let mut names: HashSet<String> = self.store.keys().map(|mi| mi.name.clone()).collect();
909        let mut to_update: BTreeMap<MipInfo, MipState> = Default::default();
910        let mut to_add: BTreeMap<MipInfo, MipState> = Default::default();
911        let mut has_error: Option<UpdateWithError> = None;
912
913        for (m_info, m_state) in store_raw.store.iter() {
914            if let Err(e) = m_state.is_consistent_with(m_info) {
915                // As soon as we found one non consistent state we abort the merge
916                has_error = Some(UpdateWithError::NonConsistent(
917                    m_info.clone(),
918                    m_state.clone(),
919                    e,
920                ));
921                break;
922            }
923
924            if let Some(m_state_orig) = self.store.get(m_info) {
925                // Given MIP info is already in self
926                // Need to check if we add it to 'to_update' list
927                let m_state_id: u32 = ComponentStateTypeId::from(&m_state.state).into();
928                let m_state_orig_id: u32 = ComponentStateTypeId::from(&m_state_orig.state).into();
929
930                // Note: we do not check for state: active OR failed OR error as they cannot change
931                if matches!(
932                    m_state_orig.state,
933                    ComponentState::Defined(_)
934                        | ComponentState::Started(_)
935                        | ComponentState::LockedIn(_)
936                ) {
937                    // Only accept 'higher' state
938                    // (e.g. 'started' if 'defined', 'locked in' if 'started'...)
939                    if m_state_id >= m_state_orig_id {
940                        to_update.insert(m_info.clone(), m_state.clone());
941                    } else {
942                        // Trying to downgrade state' (e.g. trying to go from 'active' -> 'defined')
943                        has_error = Some(UpdateWithError::Downgrade(
944                            m_info.clone(),
945                            m_state_orig.state,
946                            m_state.state,
947                        ));
948                        break;
949                    }
950                }
951            } else {
952                // Given MIP info is not in self
953                // Need to check if we add it to 'to_add' list
954
955                let last_m_info_ = to_add
956                    .last_key_value()
957                    .map(|(mi, _)| mi)
958                    .or(self.store.last_key_value().map(|(mi, _)| mi));
959
960                if let Some(last_m_info) = last_m_info_ {
961                    // check for versions of all components in v_info
962                    let mut component_version_compatible = true;
963                    for (component, component_version) in m_info.components.iter() {
964                        if component_version <= component_versions.get(component).unwrap_or(&0) {
965                            component_version_compatible = false;
966                            break;
967                        }
968                    }
969
970                    #[cfg(not(any(test, feature = "test-exports")))]
971                    if m_info.activation_delay < VERSIONING_ACTIVATION_DELAY_MIN {
972                        has_error = Some(UpdateWithError::InvalidActivationDelay(
973                            m_info.clone(),
974                            m_info.activation_delay,
975                            VERSIONING_ACTIVATION_DELAY_MIN,
976                        ));
977                        break;
978                    }
979
980                    if m_info.start > last_m_info.timeout
981                        && m_info.timeout > m_info.start
982                        && m_info.version > last_m_info.version
983                        && !names.contains(&m_info.name)
984                        && component_version_compatible
985                    {
986                        // Time range is ok / version is ok / name is unique, let's add it
987                        to_add.insert(m_info.clone(), m_state.clone());
988                        names.insert(m_info.name.clone());
989                        for (component, component_version) in m_info.components.iter() {
990                            component_versions.insert(component.clone(), *component_version);
991                        }
992                    } else {
993                        // Something is wrong (time range not ok? / version not incr? / names?
994                        // or component version not incr?)
995                        has_error = Some(UpdateWithError::Overlapping(
996                            m_info.clone(),
997                            last_m_info.clone(),
998                        ));
999                        break;
1000                    }
1001                } else {
1002                    // to_add is empty && self.0 is empty
1003                    to_add.insert(m_info.clone(), m_state.clone());
1004                    names.insert(m_info.name.clone());
1005                }
1006            }
1007        }
1008
1009        match has_error {
1010            None => {
1011                let updated: Vec<MipInfo> = to_update.keys().cloned().collect();
1012
1013                // Note: we only update the store with to_update collection
1014                //       having something in the to_add collection means that we need to update
1015                //       the Massa node software
1016                self.store.append(&mut to_update);
1017                Ok((updated, to_add))
1018            }
1019            Some(e) => Err(e),
1020        }
1021    }
1022
1023    pub fn update_network_version_stats(
1024        &mut self,
1025        slot_timestamp: MassaTime,
1026        network_versions: Option<(u32, Option<u32>)>,
1027    ) {
1028        if let Some((_current_network_version, announced_network_version_)) = network_versions {
1029            let announced_network_version = announced_network_version_.unwrap_or(0);
1030
1031            let removed_version_ = match self.stats.latest_announcements.len() {
1032                n if n >= self.stats.config.block_count_considered => {
1033                    self.stats.latest_announcements.pop_front()
1034                }
1035                _ => None,
1036            };
1037            self.stats
1038                .latest_announcements
1039                .push_back(announced_network_version);
1040
1041            // We update the count of the received version (example: update counter for version 1)
1042            let mut network_version_count = *self
1043                .stats
1044                .network_version_counters
1045                .entry(announced_network_version)
1046                .and_modify(|v| *v = v.saturating_add(1))
1047                .or_insert(1);
1048
1049            // If we removed a version announcement, we decrement the corresponding counter
1050            // (example: remove a version 1, so decrement the corresponding counter)
1051            // As soon as a counter value is 0, we remove it
1052            if let Some(removed_version) = removed_version_ {
1053                if let Entry::Occupied(mut e) =
1054                    self.stats.network_version_counters.entry(removed_version)
1055                {
1056                    let entry_value = e.get_mut();
1057                    *entry_value = entry_value.saturating_sub(1);
1058                    network_version_count = *entry_value;
1059                    if *entry_value == 0 {
1060                        self.stats.network_version_counters.remove(&removed_version);
1061                    }
1062                }
1063            }
1064
1065            if announced_network_version != 0 {
1066                let vote_ratio = Ratio::new(
1067                    network_version_count,
1068                    self.stats.config.block_count_considered as u64,
1069                );
1070
1071                if vote_ratio > self.stats.config.warn_announced_version_ratio {
1072                    let last_key_value = self.store.last_key_value();
1073                    if let Some((mi, _ms)) = last_key_value {
1074                        if announced_network_version > mi.version {
1075                            // Vote ratio is > 30%
1076                            // announced version is not known (not in MIP store)
1077                            // announced version is > to the last known network version in MIP store
1078                            // -> Warn the user to update
1079                            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.",
1080                                network_version_count,
1081                                self.stats.config.block_count_considered,
1082                                announced_network_version
1083                            );
1084                        }
1085                    }
1086                }
1087            }
1088        }
1089
1090        debug!(
1091            "[VERSIONING STATS] stats have {} counters and {} announcements",
1092            self.stats.network_version_counters.len(),
1093            self.stats.latest_announcements.len()
1094        );
1095
1096        // Even if stats did not move, update the states (e.g. LockedIn -> Active)
1097        self.advance_states_on_updated_stats(slot_timestamp);
1098    }
1099
1100    /// Used internally by `update_network_version_stats`
1101    fn advance_states_on_updated_stats(&mut self, slot_timestamp: MassaTime) {
1102        for (mi, state) in self.store.iter_mut() {
1103            if state.is_final() {
1104                // State cannot change (ex: Active), no need to update
1105                continue;
1106            }
1107
1108            let network_version_count = *self
1109                .stats
1110                .network_version_counters
1111                .get(&mi.version)
1112                .unwrap_or(&0);
1113
1114            let vote_ratio = Ratio::new(
1115                network_version_count,
1116                self.stats.config.block_count_considered as u64,
1117            );
1118
1119            debug!("[VERSIONING STATS] Vote counts / blocks considered = {} / {} (for MipInfo with network version {} - {})",
1120                network_version_count,
1121                self.stats.config.block_count_considered,
1122                mi.version,
1123                mi.name);
1124
1125            let advance_msg = Advance {
1126                start_timestamp: mi.start,
1127                timeout: mi.timeout,
1128                threshold: vote_ratio,
1129                now: slot_timestamp,
1130                activation_delay: mi.activation_delay,
1131            };
1132
1133            state.on_advance(&advance_msg.clone());
1134        }
1135    }
1136
1137    // Query
1138
1139    /// Get latest version at given timestamp (e.g. slot)
1140    fn get_latest_component_version_at(&self, component: &MipComponent, ts: MassaTime) -> u32 {
1141        let version = self
1142            .store
1143            .iter()
1144            .rev()
1145            .filter(|(mi, ms)| {
1146                mi.components.contains_key(component)
1147                    && matches!(ms.state, ComponentState::Active(_))
1148            })
1149            .find_map(|(mi, ms)| {
1150                let res = ms.state_at(ts, mi.start, mi.timeout, mi.activation_delay);
1151                match res {
1152                    Ok(ComponentStateTypeId::Active) => mi.components.get(component).copied(),
1153                    _ => None,
1154                }
1155            })
1156            .unwrap_or(0);
1157
1158        version
1159    }
1160
1161    /// Get all versions in 'Active state' for the given MipComponent
1162    fn get_all_active_component_versions(&self, component: &MipComponent) -> Vec<u32> {
1163        let versions_iter = self.store.iter().filter_map(|(mi, ms)| {
1164            if matches!(ms.state, ComponentState::Active(_)) {
1165                mi.components.get(component).copied()
1166            } else {
1167                None
1168            }
1169        });
1170        let versions: Vec<u32> = iter::once(0).chain(versions_iter).collect();
1171        versions
1172    }
1173
1174    /// Get all versions (at any state) for the given MipComponent
1175    fn get_all_component_versions(
1176        &self,
1177        component: &MipComponent,
1178    ) -> BTreeMap<u32, ComponentStateTypeId> {
1179        let versions_iter = self.store.iter().filter_map(|(mi, ms)| {
1180            mi.components
1181                .get(component)
1182                .copied()
1183                .map(|component_version| (component_version, ComponentStateTypeId::from(&ms.state)))
1184        });
1185        iter::once((0, ComponentStateTypeId::Active))
1186            .chain(versions_iter)
1187            .collect()
1188    }
1189
1190    // Network restart
1191
1192    /// Check if store is consistent with given last network shutdown
1193    /// On a network shutdown, the MIP infos will be edited but we still need to check if this is consistent
1194    /// See https://github.com/massalabs/massa-functional-tests/blob/main/tests_versioning_network_restart.py
1195    /// for an example
1196    fn is_consistent_with_shutdown_period(
1197        &self,
1198        shutdown_start: Slot,
1199        shutdown_end: Slot,
1200        thread_count: u8,
1201        t0: MassaTime,
1202        genesis_timestamp: MassaTime,
1203    ) -> Result<(), IsConsistentWithShutdownPeriodError> {
1204        // let mut is_consistent = true;
1205        let mut has_error: Result<(), IsConsistentWithShutdownPeriodError> = Ok(());
1206
1207        let shutdown_start_ts =
1208            get_block_slot_timestamp(thread_count, t0, genesis_timestamp, shutdown_start)?;
1209        let shutdown_end_ts =
1210            get_block_slot_timestamp(thread_count, t0, genesis_timestamp, shutdown_end)?;
1211        let shutdown_range = shutdown_start_ts..=shutdown_end_ts;
1212
1213        for (mip_info, mip_state) in &self.store {
1214            match mip_state.state {
1215                ComponentState::Defined(..)
1216                | ComponentState::Started(..)
1217                | ComponentState::LockedIn(..) => {
1218                    // all good if it does not start / timeout during shutdown period
1219                    if shutdown_range.contains(&mip_info.start)
1220                        || shutdown_range.contains(&mip_info.timeout)
1221                    {
1222                        // is_consistent = false;
1223                        has_error = Err(IsConsistentWithShutdownPeriodError::NonConsistent(
1224                            mip_info.clone(),
1225                            mip_state.state,
1226                            shutdown_start_ts,
1227                            shutdown_end_ts,
1228                        ));
1229                        break;
1230                    }
1231                }
1232                _ => {
1233                    // active / failed, error, nothing to do
1234                    // locked in, nothing to do (might go from 'locked in' to 'active' during shutdown)
1235                }
1236            }
1237        }
1238
1239        has_error
1240    }
1241
1242    fn update_for_network_shutdown(
1243        &mut self,
1244        shutdown_start: Slot,
1245        shutdown_end: Slot,
1246        thread_count: u8,
1247        t0: MassaTime,
1248        genesis_timestamp: MassaTime,
1249    ) -> Result<(), ModelsError> {
1250        let shutdown_start_ts =
1251            get_block_slot_timestamp(thread_count, t0, genesis_timestamp, shutdown_start)?;
1252        let shutdown_end_ts =
1253            get_block_slot_timestamp(thread_count, t0, genesis_timestamp, shutdown_end)?;
1254        let shutdown_range = shutdown_start_ts..=shutdown_end_ts;
1255
1256        let mut new_store: BTreeMap<MipInfo, MipState> = Default::default();
1257        let mut new_stats = self.stats.clone();
1258        new_stats.reset();
1259
1260        let next_valid_start_ = shutdown_end.get_next_slot(thread_count)?;
1261        let next_valid_start =
1262            get_block_slot_timestamp(thread_count, t0, genesis_timestamp, next_valid_start_)?;
1263
1264        let mut offset: Option<MassaTime> = None;
1265
1266        for (mip_info, mip_state) in &self.store {
1267            match mip_state.state {
1268                ComponentState::Defined(..) => {
1269                    // Defined: offset start & timeout
1270
1271                    let mut new_mip_info = mip_info.clone();
1272
1273                    if shutdown_range.contains(&new_mip_info.start) {
1274                        let offset_ts = match offset {
1275                            Some(offset_ts) => offset_ts,
1276                            None => {
1277                                let offset_ts = next_valid_start.saturating_sub(mip_info.start);
1278                                offset = Some(offset_ts);
1279                                offset_ts
1280                            }
1281                        };
1282
1283                        new_mip_info.start = new_mip_info.start.saturating_add(offset_ts);
1284                        new_mip_info.timeout = new_mip_info
1285                            .start
1286                            .saturating_add(mip_info.timeout.saturating_sub(mip_info.start));
1287                    }
1288                    new_store.insert(new_mip_info, mip_state.clone());
1289                }
1290                ComponentState::Started(..) | ComponentState::LockedIn(..) => {
1291                    // Started or LockedIn -> Reset to Defined, offset start & timeout
1292
1293                    let mut new_mip_info = mip_info.clone();
1294
1295                    let offset_ts = match offset {
1296                        Some(offset_ts) => offset_ts,
1297                        None => {
1298                            let offset_ts = next_valid_start.saturating_sub(mip_info.start);
1299                            offset = Some(offset_ts);
1300                            offset_ts
1301                        }
1302                    };
1303
1304                    new_mip_info.start = new_mip_info.start.saturating_add(offset_ts);
1305                    new_mip_info.timeout = new_mip_info
1306                        .start
1307                        .saturating_add(mip_info.timeout.saturating_sub(mip_info.start));
1308
1309                    // Need to reset state to 'Defined'
1310                    let new_mip_state = MipState::reset_from(mip_state)
1311                        .ok_or(ModelsError::from("Unable to reset state"))?;
1312                    // Note: statistics are already reset
1313                    new_store.insert(new_mip_info, new_mip_state.clone());
1314                }
1315                _ => {
1316                    // active / failed, error, nothing to do
1317                    new_store.insert(mip_info.clone(), mip_state.clone());
1318                }
1319            }
1320        }
1321
1322        self.store = new_store;
1323        self.stats = new_stats;
1324        Ok(())
1325    }
1326
1327    // get network versions stats
1328    // <network_version, count>
1329    pub fn get_network_versions_stats(&self) -> HashMap<u32, u64> {
1330        self.stats.network_version_counters.clone()
1331    }
1332
1333    // Final state
1334    pub fn is_key_value_valid(&self, serialized_key: &[u8], serialized_value: &[u8]) -> bool {
1335        self._is_key_value_valid(serialized_key, serialized_value)
1336            .is_ok()
1337    }
1338
1339    pub fn _is_key_value_valid(
1340        &self,
1341        serialized_key: &[u8],
1342        serialized_value: &[u8],
1343    ) -> Result<(), IsKVValidError> {
1344        let mip_info_deser = MipInfoDeserializer::new();
1345        let mip_state_deser = MipStateDeserializer::new();
1346        let mip_store_stats_deser = MipStoreStatsDeserializer::new(
1347            MIP_STORE_STATS_BLOCK_CONSIDERED,
1348            self.stats.config.warn_announced_version_ratio,
1349        );
1350
1351        if serialized_key.starts_with(MIP_STORE_PREFIX.as_bytes()) {
1352            let (rem, _mip_info) = mip_info_deser
1353                .deserialize::<DeserializeError>(&serialized_key[MIP_STORE_PREFIX.len()..])
1354                .map_err(|e| IsKVValidError::Deserialize(e.to_string()))?;
1355
1356            if !rem.is_empty() {
1357                return Err(IsKVValidError::Deserialize(
1358                    "Rem not empty after deserialization".to_string(),
1359                ));
1360            }
1361
1362            let (rem2, _mip_state) = mip_state_deser
1363                .deserialize::<DeserializeError>(serialized_value)
1364                .map_err(|e| IsKVValidError::Deserialize(e.to_string()))?;
1365
1366            if !rem2.is_empty() {
1367                return Err(IsKVValidError::Deserialize(
1368                    "Rem not empty after deserialization".to_string(),
1369                ));
1370            }
1371        } else if serialized_key.starts_with(MIP_STORE_STATS_PREFIX.as_bytes()) {
1372            let (rem, _mip_store_stats) = mip_store_stats_deser
1373                .deserialize::<DeserializeError>(serialized_value)
1374                .map_err(|e| IsKVValidError::Deserialize(e.to_string()))?;
1375
1376            if !rem.is_empty() {
1377                return Err(IsKVValidError::Deserialize(
1378                    "Rem not empty after deserialization".to_string(),
1379                ));
1380            }
1381        } else {
1382            return Err(IsKVValidError::InvalidPrefix);
1383        }
1384
1385        Ok(())
1386    }
1387
1388    // DB methods
1389
1390    /// Get MIP store changes between 2 timestamps - used by the db to update the disk
1391    pub fn update_batches(
1392        &self,
1393        batch: &mut DBBatch,
1394        versioning_batch: &mut DBBatch,
1395        between: Option<(&MassaTime, &MassaTime)>,
1396    ) -> Result<(), SerializeError> {
1397        let mip_info_ser = MipInfoSerializer::new();
1398        let mip_state_ser = MipStateSerializer::new();
1399
1400        let bounds = match between {
1401            Some(between) => (*between.0)..(*between.1),
1402            None => MassaTime::from_millis(0)..MassaTime::max(),
1403        };
1404        let mut key = Vec::new();
1405        let mut value = Vec::new();
1406
1407        for (mip_info, mip_state) in self.store.iter() {
1408            if let Some((advance, state_id)) = mip_state.history.last_key_value() {
1409                if bounds.contains(&advance.now) {
1410                    key.extend(MIP_STORE_PREFIX.as_bytes().to_vec());
1411                    mip_info_ser.serialize(mip_info, &mut key)?;
1412                    mip_state_ser.serialize(mip_state, &mut value)?;
1413                    match state_id {
1414                        ComponentStateTypeId::Active => {
1415                            batch.insert(key.clone(), Some(value.clone()));
1416                            // + "Remove key" in VERSIONING_CF
1417                            versioning_batch.insert(key.clone(), None);
1418                        }
1419                        _ => {
1420                            versioning_batch.insert(key.clone(), Some(value.clone()));
1421                        }
1422                    }
1423                    key.clear();
1424                    value.clear();
1425                }
1426            }
1427        }
1428
1429        value.clear();
1430        let mip_stats_ser = MipStoreStatsSerializer::new();
1431        mip_stats_ser.serialize(&self.stats, &mut value)?;
1432        versioning_batch.insert(
1433            MIP_STORE_STATS_PREFIX.as_bytes().to_vec(),
1434            Some(value.clone()),
1435        );
1436
1437        Ok(())
1438    }
1439
1440    /// Extend MIP store with what is written on the disk
1441    fn extend_from_db(
1442        &mut self,
1443        db: ShareableMassaDBController,
1444    ) -> Result<(Vec<MipInfo>, BTreeMap<MipInfo, MipState>), ExtendFromDbError> {
1445        let mip_info_deser = MipInfoDeserializer::new();
1446        let mip_state_deser = MipStateDeserializer::new();
1447        let mip_store_stats_deser = MipStoreStatsDeserializer::new(
1448            MIP_STORE_STATS_BLOCK_CONSIDERED,
1449            self.stats.config.warn_announced_version_ratio,
1450        );
1451
1452        let db = db.read();
1453
1454        // Get data from state cf handle
1455        let mut update_data: BTreeMap<MipInfo, MipState> = Default::default();
1456        for (ser_mip_info, ser_mip_state) in
1457            db.prefix_iterator_cf(STATE_CF, MIP_STORE_PREFIX.as_bytes())
1458        {
1459            if !ser_mip_info.starts_with(MIP_STORE_PREFIX.as_bytes()) {
1460                break;
1461            }
1462
1463            // deser
1464            let (_, mip_info) = mip_info_deser
1465                .deserialize::<DeserializeError>(&ser_mip_info[MIP_STORE_PREFIX.len()..])
1466                .map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
1467
1468            let (_, mip_state) = mip_state_deser
1469                .deserialize::<DeserializeError>(&ser_mip_state)
1470                .map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
1471
1472            update_data.insert(mip_info, mip_state);
1473        }
1474
1475        let (mut updated, mut added) = match update_data.is_empty() {
1476            true => (vec![], BTreeMap::new()),
1477            false => {
1478                let store_raw_ = MipStoreRaw {
1479                    store: update_data,
1480                    stats: MipStoreStats {
1481                        config: MipStatsConfig {
1482                            block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
1483                            warn_announced_version_ratio: self
1484                                .stats
1485                                .config
1486                                .warn_announced_version_ratio,
1487                        },
1488                        latest_announcements: Default::default(),
1489                        network_version_counters: Default::default(),
1490                    },
1491                };
1492                // Only call update_with if update_data is not empty
1493                self.update_with(&store_raw_)?
1494            }
1495        };
1496
1497        let mut update_data: BTreeMap<MipInfo, MipState> = Default::default();
1498
1499        // Get data from state cf handle
1500        for (ser_mip_info, ser_mip_state) in
1501            db.prefix_iterator_cf(VERSIONING_CF, MIP_STORE_PREFIX.as_bytes())
1502        {
1503            // deser
1504
1505            match &ser_mip_info {
1506                key if key.starts_with(MIP_STORE_PREFIX.as_bytes()) => {
1507                    let (_, mip_info) = mip_info_deser
1508                        .deserialize::<DeserializeError>(&ser_mip_info[MIP_STORE_PREFIX.len()..])
1509                        .map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
1510
1511                    let (_, mip_state) = mip_state_deser
1512                        .deserialize::<DeserializeError>(&ser_mip_state)
1513                        .map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
1514
1515                    update_data.insert(mip_info, mip_state);
1516                }
1517                key if key.starts_with(MIP_STORE_STATS_PREFIX.as_bytes()) => {
1518                    let (_, mip_store_stats) = mip_store_stats_deser
1519                        .deserialize::<DeserializeError>(&ser_mip_state)
1520                        .map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
1521
1522                    self.stats = mip_store_stats;
1523                }
1524                _ => {
1525                    break;
1526                }
1527            }
1528        }
1529
1530        if !update_data.is_empty() {
1531            let store_raw_ = MipStoreRaw {
1532                store: update_data,
1533                stats: MipStoreStats {
1534                    config: MipStatsConfig {
1535                        block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
1536                        warn_announced_version_ratio: self
1537                            .stats
1538                            .config
1539                            .warn_announced_version_ratio,
1540                    },
1541                    latest_announcements: Default::default(),
1542                    network_version_counters: Default::default(),
1543                },
1544            };
1545            // Only call update_with if update_data is not empty
1546            let (updated_2, added_2) = self.update_with(&store_raw_)?;
1547            updated.extend(updated_2);
1548            added.extend(added_2);
1549        }
1550
1551        Ok((updated, added))
1552    }
1553
1554    /// See [`MipStore::started_mips_missing_from_db`].
1555    fn started_mips_missing_from_db(
1556        &self,
1557        db: &ShareableMassaDBController,
1558        at: MassaTime,
1559    ) -> Vec<MipInfo> {
1560        let mip_info_deser = MipInfoDeserializer::new();
1561        let db = db.read();
1562
1563        // Active MIPs live in STATE_CF, the others in VERSIONING_CF: the db knows a MIP if
1564        // either holds an entry for it.
1565        let mut in_db: HashSet<String> = HashSet::new();
1566        for cf in [STATE_CF, VERSIONING_CF] {
1567            for (ser_mip_info, _) in db.prefix_iterator_cf(cf, MIP_STORE_PREFIX.as_bytes()) {
1568                if !ser_mip_info.starts_with(MIP_STORE_PREFIX.as_bytes()) {
1569                    break;
1570                }
1571                if let Ok((_, mip_info)) = mip_info_deser
1572                    .deserialize::<DeserializeError>(&ser_mip_info[MIP_STORE_PREFIX.len()..])
1573                {
1574                    in_db.insert(mip_info.name);
1575                }
1576            }
1577        }
1578
1579        self.store
1580            .keys()
1581            .filter(|mip_info| mip_info.start <= at && !in_db.contains(&mip_info.name))
1582            .cloned()
1583            .collect()
1584    }
1585
1586    /// Create a MIP store raw with what is written on the disk
1587    fn try_from_db(
1588        db: ShareableMassaDBController,
1589        cfg: MipStatsConfig,
1590    ) -> Result<Self, ExtendFromDbError> {
1591        let mut store_raw = MipStoreRaw {
1592            store: Default::default(),
1593            stats: MipStoreStats {
1594                config: cfg,
1595                latest_announcements: Default::default(),
1596                network_version_counters: Default::default(),
1597            },
1598        };
1599
1600        let (_updated, mut added) = store_raw.extend_from_db(db)?;
1601        store_raw.store.append(&mut added);
1602        Ok(store_raw)
1603    }
1604}
1605
1606impl<const N: usize> TryFrom<([(MipInfo, MipState); N], MipStatsConfig)> for MipStoreRaw {
1607    type Error = UpdateWithError;
1608
1609    fn try_from(
1610        (value, cfg): ([(MipInfo, MipState); N], MipStatsConfig),
1611    ) -> Result<Self, Self::Error> {
1612        // Build an empty store
1613        let mut store = Self {
1614            store: Default::default(),
1615            stats: MipStoreStats::new(cfg.clone()),
1616        };
1617
1618        // Build another one with given value
1619        let other_store = Self {
1620            store: BTreeMap::from(value),
1621            stats: MipStoreStats::new(cfg),
1622        };
1623
1624        // Use update_with ensuring that we have no overlapping time range, unique names & ...
1625        match store.update_with(&other_store) {
1626            Ok((_updated, mut added)) => {
1627                store.store.append(&mut added);
1628                Ok(store)
1629            }
1630            Err(e) => Err(e),
1631        }
1632    }
1633}
1634
1635// End Store
1636
1637#[cfg(test)]
1638mod test {
1639    use super::*;
1640
1641    use assert_matches::assert_matches;
1642    use massa_db_exports::{MassaDBConfig, MassaDBController, MassaIteratorMode};
1643    use massa_db_worker::MassaDB;
1644    use more_asserts::{assert_gt, assert_le};
1645    use parking_lot::RwLock;
1646    use std::ops::{Add, Sub};
1647    use std::sync::Arc;
1648    use tempfile::tempdir;
1649
1650    use crate::test_helpers::versioning_helpers::advance_state_until;
1651
1652    use massa_models::config::{MIP_STORE_STATS_BLOCK_CONSIDERED, T0, THREAD_COUNT};
1653    use massa_models::timeslots::get_closest_slot_to_timestamp;
1654
1655    // Only for unit tests
1656    impl PartialEq<ComponentState> for MipState {
1657        fn eq(&self, other: &ComponentState) -> bool {
1658            self.state == *other
1659        }
1660    }
1661
1662    // helper
1663    impl From<(&MipInfo, &Ratio<u64>, &MassaTime)> for Advance {
1664        fn from((mip_info, threshold, now): (&MipInfo, &Ratio<u64>, &MassaTime)) -> Self {
1665            Self {
1666                start_timestamp: mip_info.start,
1667                timeout: mip_info.timeout,
1668                threshold: *threshold,
1669                now: *now,
1670                activation_delay: mip_info.activation_delay,
1671            }
1672        }
1673    }
1674
1675    fn get_a_version_info() -> (MassaTime, MassaTime, MipInfo) {
1676        // A helper function to provide a default MipInfo
1677
1678        // Models a Massa Improvements Proposal (MIP-0002), transitioning component address to v2
1679
1680        let start = MassaTime::from_utc_ymd_hms(2017, 11, 1, 7, 33, 44).unwrap();
1681        let timeout = MassaTime::from_utc_ymd_hms(2017, 11, 11, 7, 33, 44).unwrap();
1682
1683        (
1684            start,
1685            timeout,
1686            MipInfo {
1687                name: "MIP-0002".to_string(),
1688                version: 2,
1689                components: BTreeMap::from([(MipComponent::Address, 1)]),
1690                start,
1691                timeout,
1692                activation_delay: MassaTime::from_millis(20),
1693            },
1694        )
1695    }
1696
1697    #[test]
1698    fn test_state_advance_from_defined() {
1699        // Test Versioning state transition (from state: Defined)
1700        let (_, _, mi) = get_a_version_info();
1701        let mut state: ComponentState = Default::default();
1702        assert_eq!(state, ComponentState::defined());
1703
1704        let now = mi.start.saturating_sub(MassaTime::from_millis(1));
1705        let mut advance_msg = Advance::from((&mi, &Ratio::zero(), &now));
1706
1707        state = state.on_advance(advance_msg.clone());
1708        assert_eq!(state, ComponentState::defined());
1709
1710        let now = mi.start.saturating_add(MassaTime::from_millis(5));
1711        advance_msg.now = now;
1712        state = state.on_advance(advance_msg);
1713
1714        // println!("state: {:?}", state);
1715        assert_eq!(
1716            state,
1717            ComponentState::Started(Started {
1718                vote_ratio: Ratio::zero()
1719            })
1720        );
1721    }
1722
1723    #[test]
1724    fn test_state_advance_from_started() {
1725        // Test Versioning state transition (from state: Started)
1726        let (_, _, mi) = get_a_version_info();
1727        let mut state: ComponentState = ComponentState::started(Default::default());
1728
1729        let now = mi.start;
1730        let threshold_too_low =
1731            VERSIONING_THRESHOLD_TRANSITION_ACCEPTED.sub(Ratio::new_raw(10, 100));
1732        let threshold_ok = VERSIONING_THRESHOLD_TRANSITION_ACCEPTED.add(Ratio::new_raw(1, 100));
1733        assert_le!(threshold_ok, Ratio::from_integer(1));
1734        let mut advance_msg = Advance::from((&mi, &threshold_too_low, &now));
1735
1736        state = state.on_advance(advance_msg.clone());
1737        assert_eq!(state, ComponentState::started(threshold_too_low));
1738        advance_msg.threshold = threshold_ok;
1739        state = state.on_advance(advance_msg);
1740        assert_eq!(state, ComponentState::locked_in(now));
1741    }
1742
1743    #[test]
1744    fn test_state_advance_from_locked_in() {
1745        // Test Versioning state transition (from state: LockedIn)
1746        let (_, _, mi) = get_a_version_info();
1747
1748        let locked_in_at = mi.start.saturating_add(MassaTime::from_millis(1));
1749        let mut state: ComponentState = ComponentState::locked_in(locked_in_at);
1750
1751        let now = mi.start;
1752        let mut advance_msg = Advance::from((&mi, &Ratio::zero(), &now));
1753
1754        state = state.on_advance(advance_msg.clone());
1755        assert_eq!(state, ComponentState::locked_in(locked_in_at));
1756
1757        advance_msg.now = advance_msg
1758            .timeout
1759            .saturating_add(MassaTime::from_millis(1));
1760        state = state.on_advance(advance_msg);
1761        assert!(matches!(state, ComponentState::Active(_)));
1762    }
1763
1764    #[test]
1765    fn test_state_advance_from_active() {
1766        // Test Versioning state transition (from state: Active)
1767        let (start, _, mi) = get_a_version_info();
1768        let mut state = ComponentState::active(start);
1769        let now = mi.start;
1770        let advance = Advance::from((&mi, &Ratio::zero(), &now));
1771
1772        state = state.on_advance(advance);
1773        assert!(matches!(state, ComponentState::Active(_)));
1774    }
1775
1776    #[test]
1777    fn test_state_advance_from_failed() {
1778        // Test Versioning state transition (from state: Failed)
1779        let (_, _, mi) = get_a_version_info();
1780        let mut state = ComponentState::failed();
1781        let now = mi.start;
1782        let advance = Advance::from((&mi, &Ratio::zero(), &now));
1783        state = state.on_advance(advance);
1784        assert_eq!(state, ComponentState::failed());
1785    }
1786
1787    #[test]
1788    fn test_state_advance_to_failed() {
1789        // Test Versioning state transition (to state: Failed)
1790        let (_, _, mi) = get_a_version_info();
1791        let now = mi.timeout.saturating_add(MassaTime::from_millis(1));
1792        let advance_msg = Advance::from((&mi, &Ratio::zero(), &now));
1793
1794        let mut state: ComponentState = Default::default(); // Defined
1795        state = state.on_advance(advance_msg.clone());
1796        assert_eq!(state, ComponentState::Failed(Failed {}));
1797
1798        let mut state = ComponentState::started(Default::default());
1799        state = state.on_advance(advance_msg.clone());
1800        assert_eq!(state, ComponentState::Failed(Failed {}));
1801    }
1802
1803    #[test]
1804    fn test_state_with_history() {
1805        // Test MipStateHistory::state_at() function
1806
1807        let (start, _, mi) = get_a_version_info();
1808        let now_0 = start;
1809        let mut state = MipState::new(now_0);
1810
1811        assert_eq!(state, ComponentState::defined());
1812
1813        let now = mi.start.saturating_add(MassaTime::from_millis(15));
1814        let mut advance_msg = Advance::from((&mi, &Ratio::zero(), &now));
1815
1816        // Move from Defined -> Started
1817        state.on_advance(&advance_msg);
1818        assert_eq!(state, ComponentState::started(Ratio::zero()));
1819
1820        // Check history
1821        assert_eq!(state.history.len(), 2);
1822        assert!(matches!(
1823            state.history.first_key_value(),
1824            Some((&AdvanceLW { .. }, &ComponentStateTypeId::Defined))
1825        ));
1826        assert!(matches!(
1827            state.history.last_key_value(),
1828            Some((&AdvanceLW { .. }, &ComponentStateTypeId::Started))
1829        ));
1830
1831        // Query with timestamp
1832
1833        // Before Defined
1834        let state_id_ = state.state_at(
1835            mi.start.saturating_sub(MassaTime::from_millis(5)),
1836            mi.start,
1837            mi.timeout,
1838            mi.activation_delay,
1839        );
1840        assert!(matches!(
1841            state_id_,
1842            Err(StateAtError::BeforeInitialState(_, _))
1843        ));
1844        // After Defined timestamp
1845        let state_id = state
1846            .state_at(mi.start, mi.start, mi.timeout, mi.activation_delay)
1847            .unwrap();
1848        assert_eq!(state_id, ComponentStateTypeId::Defined);
1849        // At Started timestamp
1850        let state_id = state
1851            .state_at(now, mi.start, mi.timeout, mi.activation_delay)
1852            .unwrap();
1853        assert_eq!(state_id, ComponentStateTypeId::Started);
1854
1855        // After Started timestamp but before timeout timestamp
1856        let after_started_ts = now.saturating_add(MassaTime::from_millis(15));
1857        let state_id_ = state.state_at(after_started_ts, mi.start, mi.timeout, mi.activation_delay);
1858        assert_eq!(state_id_, Err(StateAtError::Unpredictable));
1859
1860        // After Started timestamp and after timeout timestamp
1861        let after_timeout_ts = mi.timeout.saturating_add(MassaTime::from_millis(15));
1862        let state_id = state
1863            .state_at(after_timeout_ts, mi.start, mi.timeout, mi.activation_delay)
1864            .unwrap();
1865        assert_eq!(state_id, ComponentStateTypeId::Failed);
1866
1867        // Move from Started to LockedIn
1868        let threshold = VERSIONING_THRESHOLD_TRANSITION_ACCEPTED;
1869        advance_msg.threshold = threshold.add(Ratio::from_integer(1));
1870        advance_msg.now = now.saturating_add(MassaTime::from_millis(1));
1871        state.on_advance(&advance_msg);
1872        assert_eq!(state, ComponentState::locked_in(advance_msg.now));
1873
1874        // Query with timestamp
1875        // After LockedIn timestamp and before timeout timestamp
1876        let after_locked_in_ts = now.saturating_add(MassaTime::from_millis(10));
1877        let state_id = state
1878            .state_at(
1879                after_locked_in_ts,
1880                mi.start,
1881                mi.timeout,
1882                mi.activation_delay,
1883            )
1884            .unwrap();
1885        assert_eq!(state_id, ComponentStateTypeId::LockedIn);
1886        // After LockedIn timestamp and after timeout timestamp
1887        let state_id = state
1888            .state_at(after_timeout_ts, mi.start, mi.timeout, mi.activation_delay)
1889            .unwrap();
1890        assert_eq!(state_id, ComponentStateTypeId::Active);
1891    }
1892
1893    #[test]
1894    fn test_versioning_store_announce_current() {
1895        // Test VersioningInfo::get_version_to_announce() & ::get_version_current()
1896
1897        let (start, timeout, mi) = get_a_version_info();
1898
1899        let mut mi_2 = mi.clone();
1900        mi_2.version += 1;
1901        mi_2.start = timeout
1902            .checked_add(MassaTime::from_millis(1000 * 60 * 60 * 24 * 2))
1903            .unwrap(); // Add 2 days
1904        mi_2.timeout = timeout
1905            .checked_add(MassaTime::from_millis(1000 * 60 * 60 * 24 * 5))
1906            .unwrap(); // Add 5 days
1907
1908        // Can only build such object in test - history is empty :-/
1909        let vs_1 = MipState {
1910            state: ComponentState::active(start),
1911            history: Default::default(),
1912        };
1913        let vs_2 = MipState {
1914            state: ComponentState::started(Ratio::zero()),
1915            history: Default::default(),
1916        };
1917
1918        // TODO: Have VersioningStore::from ?
1919        let mip_stats_cfg = MipStatsConfig {
1920            block_count_considered: 10,
1921            warn_announced_version_ratio: Ratio::new_raw(30, 100),
1922        };
1923        let vs_raw = MipStoreRaw {
1924            store: BTreeMap::from([(mi.clone(), vs_1), (mi_2.clone(), vs_2)]),
1925            stats: MipStoreStats::new(mip_stats_cfg.clone()),
1926        };
1927        // let vs_raw = MipStoreRaw::try_from([(vi.clone(), vs_1), (vi_2.clone(), vs_2)]).unwrap();
1928        let vs = MipStore(Arc::new(RwLock::new(vs_raw)));
1929
1930        assert_eq!(vs.get_network_version_current(), mi.version);
1931        assert_eq!(vs.get_network_version_to_announce(), Some(mi_2.version));
1932
1933        // Test also an empty versioning store
1934        let vs_raw = MipStoreRaw {
1935            store: Default::default(),
1936            stats: MipStoreStats::new(mip_stats_cfg),
1937        };
1938        let vs = MipStore(Arc::new(RwLock::new(vs_raw)));
1939        assert_eq!(vs.get_network_version_current(), 0);
1940        assert_eq!(vs.get_network_version_to_announce(), None);
1941    }
1942
1943    #[test]
1944    fn test_is_consistent_with() {
1945        // Test MipStateHistory::is_consistent_with (consistency of MIP state against its MIP info)
1946
1947        // Given the following MIP info, we expect state
1948        // Defined @ time <= 2
1949        // Started @ time > 2 && <= 5
1950        // LockedIn @ time > time(Started) && <= 5
1951        // Active @time > 5
1952        let vi_1 = MipInfo {
1953            name: "MIP-0002".to_string(),
1954            version: 2,
1955            components: BTreeMap::from([(MipComponent::Address, 1)]),
1956            start: MassaTime::from_millis(2),
1957            timeout: MassaTime::from_millis(5),
1958            activation_delay: MassaTime::from_millis(2),
1959        };
1960        // Another versioning info (from an attacker) for testing
1961        let vi_2 = MipInfo {
1962            name: "MIP-0002".to_string(),
1963            version: 2,
1964            components: BTreeMap::from([(MipComponent::Address, 1)]),
1965            start: MassaTime::from_millis(7),
1966            timeout: MassaTime::from_millis(10),
1967            activation_delay: MassaTime::from_millis(2),
1968        };
1969
1970        let vsh = MipState {
1971            state: ComponentState::Error,
1972            history: Default::default(),
1973        };
1974        // At state Error -> (always) false
1975        assert_eq!(
1976            vsh.is_consistent_with(&vi_1),
1977            Err(IsConsistentError::AtError)
1978        );
1979
1980        let vsh = MipState {
1981            state: ComponentState::defined(),
1982            history: Default::default(),
1983        };
1984        // At state Defined but no history -> false
1985        assert!(vsh.is_consistent_with(&vi_1).is_err());
1986
1987        let mut vsh = MipState::new(MassaTime::from_millis(1));
1988        // At state Defined at time 1 -> true, given vi_1 @ time 1
1989        assert!(vsh.is_consistent_with(&vi_1).is_ok());
1990        // At state Defined at time 1 -> false given vi_1 @ time 3 (state should be Started)
1991        // assert_eq!(vsh.is_consistent_with(&vi_1, MassaTime::from_millis(3)), false);
1992
1993        // Advance to Started
1994        let now = MassaTime::from_millis(3);
1995        let adv = Advance::from((&vi_1, &Ratio::zero(), &now));
1996        vsh.on_advance(&adv);
1997        let now = MassaTime::from_millis(4);
1998        let adv = Advance::from((&vi_1, &Ratio::new_raw(14, 100), &now));
1999        vsh.on_advance(&adv);
2000
2001        // At state Started at time now -> true
2002        assert_eq!(vsh.state, ComponentState::started(Ratio::new_raw(14, 100)));
2003        assert!(vsh.is_consistent_with(&vi_1).is_ok());
2004        // Now with another versioning info
2005        assert!(vsh.is_consistent_with(&vi_2).is_err());
2006
2007        // Advance to LockedIn
2008        let now = MassaTime::from_millis(4);
2009        let adv = Advance::from((&vi_1, &VERSIONING_THRESHOLD_TRANSITION_ACCEPTED, &now));
2010        vsh.on_advance(&adv);
2011
2012        // At state LockedIn at time now -> true
2013        assert_eq!(vsh.state, ComponentState::locked_in(now));
2014        assert!(vsh.is_consistent_with(&vi_1).is_ok());
2015
2016        // edge cases
2017        // TODO: history all good but does not start with Defined, start with Started
2018    }
2019
2020    #[test]
2021    fn test_update_with() {
2022        // Test MipStoreRaw.update_with method (e.g. update a store from another, used in bootstrap)
2023
2024        let vi_1 = MipInfo {
2025            name: "MIP-0002".to_string(),
2026            version: 2,
2027            components: BTreeMap::from([(MipComponent::Address, 1)]),
2028            start: MassaTime::from_millis(2),
2029            timeout: MassaTime::from_millis(5),
2030            activation_delay: MassaTime::from_millis(2),
2031        };
2032
2033        let _time = MassaTime::now();
2034        let vs_1 = advance_state_until(ComponentState::active(_time), &vi_1);
2035        assert!(matches!(vs_1.state, ComponentState::Active(_)));
2036
2037        let vi_2 = MipInfo {
2038            name: "MIP-0003".to_string(),
2039            version: 3,
2040            components: BTreeMap::from([(MipComponent::Address, 2)]),
2041            start: MassaTime::from_millis(17),
2042            timeout: MassaTime::from_millis(27),
2043            activation_delay: MassaTime::from_millis(2),
2044        };
2045        let vs_2 = advance_state_until(ComponentState::defined(), &vi_2);
2046
2047        let mip_stats_cfg = MipStatsConfig {
2048            block_count_considered: 10,
2049            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2050        };
2051        let mut vs_raw_1 = MipStoreRaw::try_from((
2052            [(vi_1.clone(), vs_1.clone()), (vi_2.clone(), vs_2.clone())],
2053            mip_stats_cfg.clone(),
2054        ))
2055        .unwrap();
2056
2057        let vs_2_2 = advance_state_until(ComponentState::active(_time), &vi_2);
2058        assert!(matches!(vs_2_2.state, ComponentState::Active(_)));
2059
2060        let vs_raw_2 = MipStoreRaw::try_from((
2061            [(vi_1.clone(), vs_1.clone()), (vi_2.clone(), vs_2_2.clone())],
2062            mip_stats_cfg,
2063        ))
2064        .unwrap();
2065
2066        let (updated, added) = vs_raw_1.update_with(&vs_raw_2).unwrap();
2067
2068        // Check update_with result
2069        assert!(added.is_empty());
2070        assert_eq!(updated, vec![vi_2.clone()]);
2071
2072        // Expect state 1 (for vi_1) no change, state 2 (for vi_2) updated to "Active"
2073        assert_eq!(vs_raw_1.store.get(&vi_1).unwrap().state, vs_1.state);
2074        assert_eq!(vs_raw_1.store.get(&vi_2).unwrap().state, vs_2_2.state);
2075    }
2076
2077    #[test]
2078    fn test_update_with_invalid() {
2079        // Test updating a MIP store with another invalid one
2080
2081        // part 0 - defines data for the test
2082        let mi_1 = MipInfo {
2083            name: "MIP-0001".to_string(),
2084            version: 2,
2085            components: BTreeMap::from([(MipComponent::Address, 1)]),
2086            start: MassaTime::from_millis(0),
2087            timeout: MassaTime::from_millis(5),
2088            activation_delay: MassaTime::from_millis(2),
2089        };
2090        let _time = MassaTime::now();
2091        let ms_1 = advance_state_until(ComponentState::active(_time), &mi_1);
2092        assert!(matches!(ms_1.state, ComponentState::Active(_)));
2093
2094        let mi_2 = MipInfo {
2095            name: "MIP-0002".to_string(),
2096            version: 3,
2097            components: BTreeMap::from([(MipComponent::Address, 2)]),
2098            start: MassaTime::from_millis(17),
2099            timeout: MassaTime::from_millis(27),
2100            activation_delay: MassaTime::from_millis(2),
2101        };
2102        let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
2103        assert_eq!(ms_2, ComponentState::defined());
2104
2105        let mip_stats_cfg = MipStatsConfig {
2106            block_count_considered: 10,
2107            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2108        };
2109
2110        // case 1: overlapping time range
2111        {
2112            let mut store_1 = MipStoreRaw::try_from((
2113                [(mi_1.clone(), ms_1.clone()), (mi_2.clone(), ms_2.clone())],
2114                mip_stats_cfg.clone(),
2115            ))
2116            .unwrap();
2117
2118            let mut mi_2_2 = mi_2.clone();
2119            // Make mip info invalid (because start == mi_1.timeout)
2120            mi_2_2.start = mi_1.timeout;
2121            let ms_2_2 = advance_state_until(ComponentState::defined(), &mi_2_2);
2122            let store_2 = MipStoreRaw {
2123                store: BTreeMap::from([
2124                    (mi_1.clone(), ms_1.clone()),
2125                    (mi_2_2.clone(), ms_2_2.clone()),
2126                ]),
2127                stats: MipStoreStats::new(mip_stats_cfg.clone()),
2128            };
2129
2130            assert_matches!(
2131                store_1.update_with(&store_2),
2132                Err(UpdateWithError::Overlapping(..))
2133            );
2134            assert_eq!(store_1.store.get(&mi_1).unwrap().state, ms_1.state);
2135            assert_eq!(store_1.store.get(&mi_2).unwrap().state, ms_2.state);
2136
2137            // Check that try_from fails too (because it uses update_with internally)
2138            {
2139                let _store_2_ = MipStoreRaw::try_from((
2140                    [
2141                        (mi_1.clone(), ms_1.clone()),
2142                        (mi_2_2.clone(), ms_2_2.clone()),
2143                    ],
2144                    mip_stats_cfg.clone(),
2145                ));
2146                assert!(_store_2_.is_err());
2147            }
2148        }
2149
2150        // case 2: overlapping versioning component
2151        {
2152            let mut store_1 = MipStoreRaw::try_from((
2153                [(mi_1.clone(), ms_1.clone()), (mi_2.clone(), ms_2.clone())],
2154                mip_stats_cfg.clone(),
2155            ))
2156            .unwrap();
2157
2158            let mut mi_2_2 = mi_2.clone();
2159            // Make MIP invalid (has component version set to 1 - same as MIP 1)
2160            mi_2_2.components = mi_1.components.clone();
2161
2162            let ms_2_2 = advance_state_until(ComponentState::defined(), &mi_2_2);
2163            let store_2 = MipStoreRaw {
2164                store: BTreeMap::from([
2165                    (mi_1.clone(), ms_1.clone()),
2166                    (mi_2_2.clone(), ms_2_2.clone()),
2167                ]),
2168                stats: MipStoreStats::new(mip_stats_cfg.clone()),
2169            };
2170
2171            // MIP-0003 in vs_raw_1 & vs_raw_2 has != components
2172            assert_matches!(
2173                store_1.update_with(&store_2),
2174                Err(UpdateWithError::Overlapping(..))
2175            );
2176        }
2177
2178        // case 3: trying to downgrade network version
2179        {
2180            let mut store_1 =
2181                MipStoreRaw::try_from(([(mi_1.clone(), ms_1.clone())], mip_stats_cfg.clone()))
2182                    .unwrap();
2183            let mut mi_2_2 = mi_2.clone();
2184            // Make MIP 2 invalid (MIP 2 network version < MIP 1 network version)
2185            mi_2_2.version = mi_1.version - 1;
2186
2187            let store_2 = MipStoreRaw {
2188                store: BTreeMap::from([(mi_2_2.clone(), ms_2.clone())]),
2189                stats: MipStoreStats::new(mip_stats_cfg.clone()),
2190            };
2191
2192            assert_matches!(
2193                store_1.update_with(&store_2),
2194                Err(UpdateWithError::Overlapping(..))
2195            );
2196
2197            // Test again but with == network versions
2198
2199            let mut mi_2_3 = mi_2.clone();
2200            // Make MIP 2 invalid (MIP 2 network version == MIP 1 network version)
2201            mi_2_3.version = mi_1.version;
2202
2203            let store_2 = MipStoreRaw {
2204                store: BTreeMap::from([(mi_2_3.clone(), ms_2.clone())]),
2205                stats: MipStoreStats::new(mip_stats_cfg.clone()),
2206            };
2207
2208            assert_matches!(
2209                store_1.update_with(&store_2),
2210                Err(UpdateWithError::Overlapping(..))
2211            );
2212        }
2213
2214        // case 4: non unique name
2215        {
2216            let mut store_1 =
2217                MipStoreRaw::try_from(([(mi_1.clone(), ms_1.clone())], mip_stats_cfg.clone()))
2218                    .unwrap();
2219            let mut mi_2_2 = mi_2.clone();
2220            // Make MIP 2 invalid (MIP 2 name == MIP 1 name)
2221            mi_2_2.name = mi_1.name.clone();
2222
2223            let store_2 = MipStoreRaw {
2224                store: BTreeMap::from([(mi_2_2.clone(), ms_2.clone())]),
2225                stats: MipStoreStats::new(mip_stats_cfg.clone()),
2226            };
2227
2228            assert_matches!(
2229                store_1.update_with(&store_2),
2230                Err(UpdateWithError::Overlapping(..))
2231            );
2232        }
2233
2234        // case 5: trying to downgrade state
2235        {
2236            let ms_1_1 =
2237                advance_state_until(ComponentState::locked_in(MassaTime::from_millis(0)), &mi_1);
2238            let ms_1_2 = advance_state_until(ComponentState::started(Ratio::zero()), &mi_1);
2239            let mut store_1 =
2240                MipStoreRaw::try_from(([(mi_1.clone(), ms_1_1.clone())], mip_stats_cfg.clone()))
2241                    .unwrap();
2242
2243            // Try to downgrade from 'LockedIn' to 'Started'
2244            let store_2 = MipStoreRaw {
2245                store: BTreeMap::from([(mi_1.clone(), ms_1_2.clone())]),
2246                stats: MipStoreStats::new(mip_stats_cfg.clone()),
2247            };
2248
2249            assert_matches!(
2250                store_1.update_with(&store_2),
2251                Err(UpdateWithError::Downgrade(..))
2252            );
2253        }
2254    }
2255
2256    #[test]
2257    fn test_try_from_invalid() {
2258        // Test create a MIP store with invalid MIP info
2259
2260        // part 0 - defines data for the test
2261        let mip_stats_cfg = MipStatsConfig {
2262            block_count_considered: 10,
2263            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2264        };
2265        let mi_1 = MipInfo {
2266            name: "MIP-0002".to_string(),
2267            version: 2,
2268            components: BTreeMap::from([(MipComponent::Address, 1)]),
2269            start: MassaTime::from_millis(0),
2270            timeout: MassaTime::from_millis(5),
2271            activation_delay: MassaTime::from_millis(2),
2272        };
2273        let _time = MassaTime::now();
2274        let ms_1 = advance_state_until(ComponentState::active(_time), &mi_1);
2275        assert!(matches!(ms_1.state, ComponentState::Active(_)));
2276        {
2277            // make mi_1_1 invalid: start is after timeout
2278            let mut mi_1_1 = mi_1.clone();
2279            mi_1_1.start = MassaTime::from_millis(5);
2280            mi_1_1.timeout = MassaTime::from_millis(2);
2281
2282            let mip_store =
2283                MipStoreRaw::try_from(([(mi_1_1, ms_1.clone())], mip_stats_cfg.clone()));
2284            assert_matches!(mip_store, Err(UpdateWithError::NonConsistent(..)));
2285        }
2286        {
2287            let ms_1_2 = MipState::new(MassaTime::from_millis(15));
2288            // make mi_1_1 invalid: start is after timeout
2289            let mut mi_1_2 = mi_1.clone();
2290            mi_1_2.start = MassaTime::from_millis(2);
2291            mi_1_2.timeout = MassaTime::from_millis(5);
2292
2293            let mip_store = MipStoreRaw::try_from(([(mi_1_2, ms_1_2)], mip_stats_cfg.clone()));
2294            assert_matches!(mip_store, Err(UpdateWithError::NonConsistent(..)));
2295        }
2296        {
2297            let ms_1_2 = MipState::new(MassaTime::from_millis(15));
2298            // make mi_1_1 invalid: start is after timeout
2299            let mut mi_1_2 = mi_1.clone();
2300            mi_1_2.start = MassaTime::from_millis(16);
2301            mi_1_2.timeout = MassaTime::from_millis(5);
2302
2303            let mip_store = MipStoreRaw::try_from(([(mi_1_2, ms_1_2)], mip_stats_cfg));
2304            assert_matches!(mip_store, Err(UpdateWithError::NonConsistent(..)));
2305        }
2306    }
2307
2308    #[test]
2309    fn test_empty_mip_store() {
2310        // Test if we can init an empty MipStore
2311
2312        let mip_stats_config = MipStatsConfig {
2313            block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
2314            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2315        };
2316
2317        let mip_store = MipStore::try_from(([], mip_stats_config));
2318        assert!(mip_store.is_ok());
2319    }
2320
2321    #[test]
2322    fn test_update_with_unknown() {
2323        // Test update_with with unknown MipComponent (can happen if a node software is outdated)
2324
2325        // data
2326        let mip_stats_config = MipStatsConfig {
2327            block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
2328            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2329        };
2330
2331        let mut mip_store_raw_1 = MipStoreRaw::try_from(([], mip_stats_config.clone())).unwrap();
2332
2333        let mi_1 = MipInfo {
2334            name: "MIP-0002".to_string(),
2335            version: 2,
2336            components: BTreeMap::from([(MipComponent::__Nonexhaustive, 1)]),
2337            start: MassaTime::from_millis(0),
2338            timeout: MassaTime::from_millis(5),
2339            activation_delay: MassaTime::from_millis(2),
2340        };
2341        let ms_1 = advance_state_until(ComponentState::defined(), &mi_1);
2342        assert_eq!(ms_1, ComponentState::defined());
2343        let mip_store_raw_2 = MipStoreRaw {
2344            store: BTreeMap::from([(mi_1.clone(), ms_1.clone())]),
2345            stats: MipStoreStats::new(mip_stats_config.clone()),
2346        };
2347
2348        let (updated, added) = mip_store_raw_1.update_with(&mip_store_raw_2).unwrap();
2349
2350        assert_eq!(updated.len(), 0);
2351        assert_eq!(added.len(), 1);
2352        assert_eq!(added.get(&mi_1).unwrap().state, ComponentState::defined());
2353    }
2354
2355    #[test]
2356    fn test_mip_store_network_restart() {
2357        // Test if we can get a consistent MipStore after a network shutdown
2358
2359        let genesis_timestamp = MassaTime::from_millis(0);
2360
2361        // helper functions so the test code is easy to read
2362        let get_slot_ts =
2363            |slot| get_block_slot_timestamp(THREAD_COUNT, T0, genesis_timestamp, slot).unwrap();
2364        let is_consistent = |store: &MipStoreRaw, shutdown_start, shutdown_end| {
2365            store.is_consistent_with_shutdown_period(
2366                shutdown_start,
2367                shutdown_end,
2368                THREAD_COUNT,
2369                T0,
2370                genesis_timestamp,
2371            )
2372        };
2373        let update_store = |store: &mut MipStoreRaw, shutdown_start, shutdown_end| {
2374            store
2375                .update_for_network_shutdown(
2376                    shutdown_start,
2377                    shutdown_end,
2378                    THREAD_COUNT,
2379                    T0,
2380                    genesis_timestamp,
2381                )
2382                .unwrap()
2383        };
2384        let _dump_store = |store: &MipStoreRaw| {
2385            println!("Dump store:");
2386            for (mip_info, mip_state) in store.store.iter() {
2387                println!(
2388                    "mip_info {} {} - start: {} - timeout: {}: state: {:?}",
2389                    mip_info.name,
2390                    mip_info.version,
2391                    get_closest_slot_to_timestamp(
2392                        THREAD_COUNT,
2393                        T0,
2394                        genesis_timestamp,
2395                        mip_info.start
2396                    ),
2397                    get_closest_slot_to_timestamp(
2398                        THREAD_COUNT,
2399                        T0,
2400                        genesis_timestamp,
2401                        mip_info.timeout
2402                    ),
2403                    mip_state.state
2404                );
2405            }
2406        };
2407        // end helpers
2408
2409        let shutdown_start = Slot::new(2, 0);
2410        let shutdown_end = Slot::new(8, 0);
2411
2412        let mip_stats_cfg = MipStatsConfig {
2413            block_count_considered: 10,
2414            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2415        };
2416        let mut mi_1 = MipInfo {
2417            name: "MIP-0002".to_string(),
2418            version: 2,
2419            components: BTreeMap::from([(MipComponent::Address, 1)]),
2420            start: MassaTime::from_millis(2),
2421            timeout: MassaTime::from_millis(5),
2422            activation_delay: MassaTime::from_millis(100),
2423        };
2424        let mut mi_2 = MipInfo {
2425            name: "MIP-0003".to_string(),
2426            version: 3,
2427            components: BTreeMap::from([(MipComponent::Address, 2)]),
2428            start: MassaTime::from_millis(7),
2429            timeout: MassaTime::from_millis(11),
2430            activation_delay: MassaTime::from_millis(100),
2431        };
2432
2433        // MipInfo 1 @ state 'Defined' should start during shutdown
2434        {
2435            mi_1.start = get_slot_ts(Slot::new(3, 7));
2436            mi_1.timeout = get_slot_ts(Slot::new(5, 7));
2437            mi_2.start = get_slot_ts(Slot::new(7, 7));
2438            mi_2.timeout = get_slot_ts(Slot::new(10, 7));
2439
2440            let ms_1 = advance_state_until(ComponentState::defined(), &mi_1);
2441            let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
2442            assert_eq!(ms_1, ComponentState::defined());
2443            assert_eq!(ms_2, ComponentState::defined());
2444
2445            let mut store = MipStoreRaw::try_from((
2446                [(mi_1.clone(), ms_1), (mi_2.clone(), ms_2)],
2447                mip_stats_cfg.clone(),
2448            ))
2449            .unwrap();
2450
2451            match is_consistent(&store, shutdown_start, shutdown_end) {
2452                Err(IsConsistentWithShutdownPeriodError::NonConsistent(mi, ..)) => {
2453                    assert_eq!(mi, mi_1);
2454                }
2455                _ => panic!("is_consistent expects a non consistent error"),
2456            }
2457
2458            update_store(&mut store, shutdown_start, shutdown_end);
2459            assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
2460            // _dump_store(&store);
2461        }
2462
2463        // MipInfo 1 @ state 'Defined' will start AFTER shutdown
2464        {
2465            mi_1.start = get_slot_ts(Slot::new(9, 7));
2466            mi_1.timeout = get_slot_ts(Slot::new(11, 7));
2467            mi_2.start = get_slot_ts(Slot::new(12, 7));
2468            mi_2.timeout = get_slot_ts(Slot::new(19, 7));
2469
2470            let ms_1 = advance_state_until(ComponentState::defined(), &mi_1);
2471            let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
2472            assert_eq!(ms_1, ComponentState::defined());
2473            assert_eq!(ms_2, ComponentState::defined());
2474            let mut store = MipStoreRaw::try_from((
2475                [(mi_1.clone(), ms_1), (mi_2.clone(), ms_2)],
2476                mip_stats_cfg.clone(),
2477            ))
2478            .unwrap();
2479            let store_orig = store.clone();
2480
2481            // Already ok even with a shutdown but let's check it
2482            assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
2483            // _dump_store(&store);
2484            update_store(&mut store, shutdown_start, shutdown_end);
2485            assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
2486            // _dump_store(&store);
2487
2488            // Check that nothing has changed
2489            assert_eq!(store_orig, store);
2490        }
2491
2492        // MipInfo 1 @ state 'Started' before shutdown
2493        {
2494            mi_1.start = get_slot_ts(Slot::new(1, 7));
2495            mi_1.timeout = get_slot_ts(Slot::new(5, 7));
2496            mi_2.start = get_slot_ts(Slot::new(7, 7));
2497            mi_2.timeout = get_slot_ts(Slot::new(10, 7));
2498
2499            let ms_1 = advance_state_until(ComponentState::started(Ratio::zero()), &mi_1);
2500            let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
2501            assert_eq!(ms_1, ComponentState::started(Ratio::zero()));
2502            assert_eq!(ms_2, ComponentState::defined());
2503            let mut store = MipStoreRaw::try_from((
2504                [(mi_1.clone(), ms_1), (mi_2.clone(), ms_2)],
2505                mip_stats_cfg.clone(),
2506            ))
2507            .unwrap();
2508
2509            assert_matches!(
2510                is_consistent(&store, shutdown_start, shutdown_end),
2511                Err(IsConsistentWithShutdownPeriodError::NonConsistent(..))
2512            );
2513            update_store(&mut store, shutdown_start, shutdown_end);
2514            assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
2515            // _dump_store(&store);
2516        }
2517
2518        // MipInfo 1 @ state 'Started' after shutdown
2519        {
2520            mi_1.start = get_slot_ts(Slot::new(9, 2));
2521            mi_1.timeout = get_slot_ts(Slot::new(10, 7));
2522            mi_2.start = get_slot_ts(Slot::new(11, 4));
2523            mi_2.timeout = get_slot_ts(Slot::new(12, 0));
2524
2525            let ms_1 = advance_state_until(ComponentState::started(Ratio::zero()), &mi_1);
2526            let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
2527            assert_eq!(ms_1, ComponentState::started(Ratio::zero()));
2528            assert_eq!(ms_2, ComponentState::defined());
2529            let mut store = MipStoreRaw::try_from((
2530                [(mi_1.clone(), ms_1), (mi_2.clone(), ms_2)],
2531                mip_stats_cfg.clone(),
2532            ))
2533            .unwrap();
2534
2535            assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
2536            update_store(&mut store, shutdown_start, shutdown_end);
2537            assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
2538            // _dump_store(&store);
2539        }
2540
2541        // MipInfo 1 @ state 'LockedIn' with transition during shutdown
2542        {
2543            let shutdown_range = shutdown_start..=shutdown_end;
2544
2545            mi_1.start = get_slot_ts(Slot::new(1, 7));
2546            mi_1.timeout = get_slot_ts(Slot::new(5, 7));
2547
2548            // Just before shutdown
2549            let locked_in_at = Slot::new(1, 9);
2550            assert!(locked_in_at < shutdown_start);
2551            let activate_at = Slot::new(4, 0);
2552            assert!(shutdown_range.contains(&activate_at));
2553            // MIP 1 in state 'LockedIn', should transition to 'Active' during shutdown period
2554            mi_1.activation_delay =
2555                get_slot_ts(activate_at).saturating_sub(get_slot_ts(locked_in_at));
2556            let ms_1 = advance_state_until(
2557                ComponentState::locked_in(get_slot_ts(Slot::new(1, 9))),
2558                &mi_1,
2559            );
2560
2561            // MIP 2 in state 'Defined'
2562            mi_2.start = get_slot_ts(Slot::new(7, 7));
2563            mi_2.timeout = get_slot_ts(Slot::new(10, 7));
2564            let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
2565            let mut store = MipStoreRaw::try_from((
2566                [(mi_1.clone(), ms_1), (mi_2.clone(), ms_2)],
2567                mip_stats_cfg.clone(),
2568            ))
2569            .unwrap();
2570
2571            match is_consistent(&store, shutdown_start, shutdown_end) {
2572                Err(IsConsistentWithShutdownPeriodError::NonConsistent(mi, ..)) => {
2573                    assert_eq!(mi, mi_1);
2574                }
2575                _ => panic!("is_consistent expects a non consistent error"),
2576            }
2577            // _dump_store(&store);
2578            update_store(&mut store, shutdown_start, shutdown_end);
2579            assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
2580            // _dump_store(&store);
2581
2582            // Update stats - so should force transitions if any
2583            store.update_network_version_stats(
2584                get_slot_ts(shutdown_end.get_next_slot(THREAD_COUNT).unwrap()),
2585                Some((1, None)),
2586            );
2587
2588            let (first_mi_info, first_mi_state) = store.store.first_key_value().unwrap();
2589            assert_eq!(*first_mi_info.name, mi_1.name);
2590            // State was 'LockedIn' -> reset, start ts now defined right after network restart
2591            assert_eq!(
2592                ComponentStateTypeId::from(&first_mi_state.state),
2593                ComponentStateTypeId::Started
2594            );
2595            let (last_mi_info, last_mi_state) = store.store.last_key_value().unwrap();
2596            assert_eq!(*last_mi_info.name, mi_2.name);
2597            // State was 'Defined' -> start is set up after MIP 1 start & timeout
2598            assert_eq!(
2599                ComponentStateTypeId::from(&last_mi_state.state),
2600                ComponentStateTypeId::Defined
2601            );
2602        }
2603    }
2604
2605    #[test]
2606    fn test_started_mips_missing_from_db() {
2607        // A bootstrap server on an older release streams a MIP store without the newest MIP.
2608        // The client must flag that MIP once its vote has started by the bootstrapped slot,
2609        // and only then: before `start`, replaying it locally is deterministic.
2610
2611        let genesis_timestamp = MassaTime::from_millis(0);
2612        let get_slot_ts =
2613            |slot| get_block_slot_timestamp(THREAD_COUNT, T0, genesis_timestamp, slot).unwrap();
2614
2615        let temp_dir = tempdir().expect("Unable to create a temp folder");
2616        let db_config = MassaDBConfig {
2617            path: temp_dir.path().to_path_buf(),
2618            max_history_length: 100,
2619            max_final_state_elements_size: 100_000,
2620            max_versioning_elements_size: 100_000,
2621            thread_count: THREAD_COUNT,
2622            max_ledger_backups: 10,
2623            enable_metrics: false,
2624        };
2625        let db = Arc::new(RwLock::new(
2626            Box::new(MassaDB::new(db_config)) as Box<dyn MassaDBController + 'static>
2627        ));
2628
2629        // Known to both releases, Active: streamed through STATE_CF.
2630        let mi_old = MipInfo {
2631            name: "MIP-0002".to_string(),
2632            version: 2,
2633            components: BTreeMap::from([(MipComponent::Address, 1)]),
2634            start: get_slot_ts(Slot::new(2, 0)),
2635            timeout: get_slot_ts(Slot::new(3, 0)),
2636            activation_delay: MassaTime::from_millis(10),
2637        };
2638        let ms_old = advance_state_until(
2639            ComponentState::active(get_slot_ts(Slot::new(2, 5))),
2640            &mi_old,
2641        );
2642        // Known only to the newer (client) release.
2643        let mi_new = MipInfo {
2644            name: "MIP-0003".to_string(),
2645            version: 3,
2646            components: BTreeMap::from([(MipComponent::Address, 2)]),
2647            start: get_slot_ts(Slot::new(4, 2)),
2648            timeout: get_slot_ts(Slot::new(7, 2)),
2649            activation_delay: MassaTime::from_millis(10),
2650        };
2651        let ms_new = advance_state_until(ComponentState::defined(), &mi_new);
2652
2653        let mip_stats_config = MipStatsConfig {
2654            block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
2655            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2656        };
2657
2658        // What the old server streams: its own store, which never heard of MIP-0003.
2659        let server_store =
2660            MipStore::try_from(([(mi_old.clone(), ms_old)], mip_stats_config.clone()))
2661                .expect("Cannot create the server MIP store");
2662        let mut db_batch = DBBatch::new();
2663        let mut db_versioning_batch = DBBatch::new();
2664        server_store
2665            .update_batches(&mut db_batch, &mut db_versioning_batch, None)
2666            .unwrap();
2667        db.write()
2668            .write_batch(db_batch, db_versioning_batch, Some(Slot::new(3, 0)));
2669
2670        let client_store = MipStore::try_from((
2671            [
2672                (
2673                    mi_old.clone(),
2674                    advance_state_until(ComponentState::defined(), &mi_old),
2675                ),
2676                (mi_new.clone(), ms_new),
2677            ],
2678            mip_stats_config,
2679        ))
2680        .expect("Cannot create the client MIP store");
2681
2682        // Bootstrapped before MIP-0003 starts: nothing recorded yet anywhere, fine.
2683        let before_start = get_slot_ts(Slot::new(4, 1));
2684        assert!(client_store
2685            .started_mips_missing_from_db(&db, before_start)
2686            .is_empty());
2687
2688        // Bootstrapped at or after its start: the server cannot provide its state.
2689        for at in [mi_new.start, get_slot_ts(Slot::new(5, 0))] {
2690            assert_eq!(
2691                client_store.started_mips_missing_from_db(&db, at),
2692                vec![mi_new.clone()]
2693            );
2694        }
2695
2696        // A MIP the server does stream is never reported, however late the bootstrap.
2697        assert!(!client_store
2698            .started_mips_missing_from_db(&db, get_slot_ts(Slot::new(9, 0)))
2699            .contains(&mi_old));
2700    }
2701
2702    #[test]
2703    fn test_mip_store_db() {
2704        // Test interaction of MIP store with MassaDB
2705        // 1- init from db (empty disk)
2706        // 2- update state
2707        // 3- write changes to db
2708        // 4- init a new mip store from disk and compare
2709        // 5- test is_key_value_valid method
2710
2711        let genesis_timestamp = MassaTime::from_millis(0);
2712        // helpers
2713        let get_slot_ts =
2714            |slot| get_block_slot_timestamp(THREAD_COUNT, T0, genesis_timestamp, slot).unwrap();
2715
2716        // Db init
2717
2718        let temp_dir = tempdir().expect("Unable to create a temp folder");
2719        // println!("Using temp dir: {:?}", temp_dir.path());
2720
2721        let db_config = MassaDBConfig {
2722            path: temp_dir.path().to_path_buf(),
2723            max_history_length: 100,
2724            max_final_state_elements_size: 100_000,
2725            max_versioning_elements_size: 100_000,
2726            thread_count: THREAD_COUNT,
2727            max_ledger_backups: 10,
2728            enable_metrics: false,
2729        };
2730        let db = Arc::new(RwLock::new(
2731            Box::new(MassaDB::new(db_config)) as Box<dyn MassaDBController + 'static>
2732        ));
2733
2734        // MIP info / store init
2735
2736        let mi_1 = MipInfo {
2737            name: "MIP-0002".to_string(),
2738            version: 2,
2739            components: BTreeMap::from([(MipComponent::Address, 1)]),
2740            start: get_slot_ts(Slot::new(2, 0)),
2741            timeout: get_slot_ts(Slot::new(3, 0)),
2742            activation_delay: MassaTime::from_millis(10),
2743        };
2744        let ms_1 = advance_state_until(ComponentState::defined(), &mi_1);
2745
2746        let mi_2 = MipInfo {
2747            name: "MIP-0003".to_string(),
2748            version: 3,
2749            components: BTreeMap::from([(MipComponent::Address, 2)]),
2750            start: get_slot_ts(Slot::new(4, 2)),
2751            timeout: get_slot_ts(Slot::new(7, 2)),
2752            activation_delay: MassaTime::from_millis(10),
2753        };
2754        let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
2755
2756        let mip_stats_config = MipStatsConfig {
2757            block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
2758            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2759        };
2760        let mut mip_store = MipStore::try_from((
2761            [(mi_1.clone(), ms_1.clone()), (mi_2.clone(), ms_2.clone())],
2762            mip_stats_config.clone(),
2763        ))
2764        .expect("Cannot create an empty MIP store");
2765
2766        // Step 1
2767
2768        mip_store.extend_from_db(db.clone()).unwrap();
2769        // Check that we extend from an empty folder
2770        assert_eq!(mip_store.0.read().store.len(), 2);
2771        assert_eq!(
2772            mip_store.0.read().store.first_key_value(),
2773            Some((&mi_1, &ms_1))
2774        );
2775        assert_eq!(
2776            mip_store.0.read().store.last_key_value(),
2777            Some((&mi_2, &ms_2))
2778        );
2779
2780        // Step 2
2781        let active_at = get_slot_ts(Slot::new(2, 5));
2782        let ms_1_ = advance_state_until(ComponentState::active(active_at), &mi_1);
2783
2784        let mip_store_ =
2785            MipStore::try_from(([(mi_1.clone(), ms_1_.clone())], mip_stats_config.clone()))
2786                .expect("Cannot create an empty MIP store");
2787
2788        let (updated, added) = mip_store.update_with(&mip_store_).unwrap();
2789
2790        // Check update_with result - only 1 state should be updated
2791        assert_eq!(updated.len(), 1);
2792        assert_eq!(added.len(), 0);
2793        assert_eq!(mip_store.0.read().store.len(), 2);
2794        assert_eq!(
2795            mip_store.0.read().store.first_key_value(),
2796            Some((&mi_1, &ms_1_))
2797        );
2798        assert_eq!(
2799            mip_store.0.read().store.last_key_value(),
2800            Some((&mi_2, &ms_2))
2801        );
2802
2803        // Step 3
2804
2805        let mut db_batch = DBBatch::new();
2806        let mut db_versioning_batch = DBBatch::new();
2807
2808        // FIXME: get slot right after active at - no hardcode
2809        let slot_bounds_ = (&Slot::new(1, 0), &Slot::new(4, 2));
2810        let between = (&get_slot_ts(*slot_bounds_.0), &get_slot_ts(*slot_bounds_.1));
2811
2812        mip_store
2813            .update_batches(&mut db_batch, &mut db_versioning_batch, Some(between))
2814            .unwrap();
2815
2816        assert_eq!(db_batch.len(), 1); // mi_1
2817        assert_eq!(db_versioning_batch.len(), 3); // mi_2 + mi_1 removal + stats
2818
2819        let mut guard_db = db.write();
2820        // FIXME / TODO: no slot hardcoding?
2821        guard_db.write_batch(db_batch, db_versioning_batch, Some(Slot::new(3, 0)));
2822        drop(guard_db);
2823
2824        // Step 4
2825        let mut mip_store_2 = MipStore::try_from((
2826            [(mi_1.clone(), ms_1.clone()), (mi_2.clone(), ms_2.clone())],
2827            mip_stats_config.clone(),
2828        ))
2829        .expect("Cannot create an empty MIP store");
2830        // assert_eq!(mip_store_2.0.read().store.len(), 0);
2831
2832        mip_store_2.extend_from_db(db.clone()).unwrap();
2833
2834        let guard_1 = mip_store.0.read();
2835        let guard_2 = mip_store_2.0.read();
2836        let st1_raw = guard_1.deref();
2837        let st2_raw = guard_2.deref();
2838
2839        // println!("st1_raw: {:?}", st1_raw);
2840        // println!("st2_raw: {:?}", st2_raw);
2841        assert_eq!(st1_raw, st2_raw);
2842
2843        // Step 5: test is_key_value_valid
2844        let mut count = 0;
2845        for (ser_key, ser_value) in db
2846            .read()
2847            .iterator_cf_for_full_db_traversal(STATE_CF, MassaIteratorMode::Start)
2848        {
2849            assert!(mip_store.is_key_value_valid(&ser_key, &ser_value));
2850            count += 1;
2851        }
2852        assert_gt!(count, 0);
2853
2854        let mut count2 = 0;
2855        for (ser_key, ser_value) in db
2856            .read()
2857            .iterator_cf_for_full_db_traversal(VERSIONING_CF, MassaIteratorMode::Start)
2858        {
2859            assert!(mip_store.is_key_value_valid(&ser_key, &ser_value));
2860            count2 += 1;
2861        }
2862        assert_gt!(count2, 0);
2863    }
2864
2865    #[test]
2866    fn test_mip_store_stats() {
2867        // Test MipStoreRaw stats
2868
2869        // helper functions so the test code is easy to read
2870        let genesis_timestamp = MassaTime::from_millis(0);
2871        let get_slot_ts =
2872            |slot| get_block_slot_timestamp(THREAD_COUNT, T0, genesis_timestamp, slot).unwrap();
2873
2874        let mip_stats_config = MipStatsConfig {
2875            block_count_considered: 2,
2876            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2877        };
2878        let activation_delay = MassaTime::from_millis(100);
2879        let timeout = MassaTime::now().saturating_add(MassaTime::from_millis(50_000)); // + 50 seconds
2880        let mi_1 = MipInfo {
2881            name: "MIP-0001".to_string(),
2882            version: 1,
2883            components: BTreeMap::from([(MipComponent::Address, 1)]),
2884            start: MassaTime::from_millis(2),
2885            timeout,
2886            activation_delay,
2887        };
2888        let ms_1 = advance_state_until(ComponentState::started(Ratio::zero()), &mi_1);
2889
2890        let mut mip_store =
2891            MipStoreRaw::try_from(([(mi_1.clone(), ms_1)], mip_stats_config)).unwrap();
2892
2893        // Current network version is 0, next one is 1
2894        mip_store.update_network_version_stats(get_slot_ts(Slot::new(1, 0)), Some((0, Some(1))));
2895        assert_eq!(mip_store.stats.network_version_counters.len(), 1);
2896        assert_eq!(mip_store.stats.network_version_counters.get(&1), Some(&1));
2897
2898        mip_store.update_network_version_stats(get_slot_ts(Slot::new(1, 0)), Some((0, Some(1))));
2899        assert_eq!(mip_store.stats.network_version_counters.len(), 1);
2900        assert_eq!(mip_store.stats.network_version_counters.get(&1), Some(&2));
2901
2902        // Check that MipInfo is now
2903        let (mi_, ms_) = mip_store.store.last_key_value().unwrap();
2904        assert_eq!(*mi_, mi_1);
2905        assert_matches!(ms_.state, ComponentState::LockedIn(..));
2906
2907        let mut at = MassaTime::now();
2908        at = at.saturating_add(activation_delay);
2909        assert_eq!(
2910            ms_.state_at(at, mi_1.start, mi_1.timeout, mi_1.activation_delay),
2911            Ok(ComponentStateTypeId::Active)
2912        );
2913
2914        // Now network version is 1, next one is 2
2915        mip_store.update_network_version_stats(get_slot_ts(Slot::new(1, 0)), Some((1, Some(2))));
2916        // Counter for announced version: 1 & 2
2917        assert_eq!(mip_store.stats.network_version_counters.len(), 2);
2918        // First announced version 1 was removed and so the counter decremented
2919        assert_eq!(mip_store.stats.network_version_counters.get(&1), Some(&1));
2920        assert_eq!(mip_store.stats.network_version_counters.get(&2), Some(&1));
2921    }
2922}