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, as the store's state says right now.
575    ///
576    /// This follows the store's *current* state, which only reaches Active once
577    /// finality has processed the activation slot. Block headers must not use it:
578    /// see [`MipStore::get_network_version_active_at`].
579    pub fn get_network_version_current(&self) -> u32 {
580        let lock = self.0.read();
581        let store = lock.deref();
582        // Current version == last active
583        store
584            .store
585            .iter()
586            .rev()
587            .find_map(|(k, v)| (matches!(v.state, ComponentState::Active(_))).then_some(k.version))
588            .unwrap_or(0)
589    }
590
591    /// Retrieve the last active version at the given timestamp (e.g. a slot's).
592    ///
593    /// A MIP still `LockedIn` in this store counts as active at `ts` once
594    /// `ts > locked_at + activation_delay`. That is exactly when
595    /// `LockedIn::on_advance` activates it, and every node advances the store on
596    /// every final slot, so `Active.at` is always the first slot timestamp past
597    /// that instant. Answering from the instant rather than from the state means a
598    /// node whose finality has not yet reached the activation slot agrees with a
599    /// node that is already Active -- and with the version the producer stamped --
600    /// for every slot. Otherwise the blocks produced during the finality lag carry
601    /// the old version, and any node that is already Active (e.g. one that just
602    /// bootstrapped) rejects them.
603    ///
604    /// Requires `activation_delay` to exceed the finality lag, so that nodes know a
605    /// MIP is LockedIn before its activation instant (guaranteed on real networks by
606    /// `VERSIONING_ACTIVATION_DELAY_MIN`, one cycle).
607    pub fn get_network_version_active_at(&self, ts: MassaTime) -> u32 {
608        let lock = self.0.read();
609        let store = lock.deref();
610        store
611            .store
612            .iter()
613            .rev()
614            .find_map(|(k, v)| match v.state {
615                ComponentState::Active(Active { at }) if at <= ts => Some(k.version),
616                ComponentState::LockedIn(LockedIn { at })
617                    if ts > at.saturating_add(k.activation_delay) =>
618                {
619                    Some(k.version)
620                }
621                _ => None,
622            })
623            .unwrap_or(0)
624    }
625
626    /// Retrieve the network version to announce in the header of a block at `ts`.
627    ///
628    /// Same as [`MipStore::get_network_version_to_announce`], except that a MIP
629    /// already active at `ts` (see [`MipStore::get_network_version_active_at`]) is
630    /// no longer announced: it is the header's current version from then on, and
631    /// announcing it as well would make the header invalid.
632    pub fn get_network_version_to_announce_at(&self, ts: MassaTime) -> Option<u32> {
633        let lock = self.0.read();
634        let store = lock.deref();
635        store.store.iter().rev().find_map(|(k, v)| match v.state {
636            ComponentState::Started(_) => Some(k.version),
637            ComponentState::LockedIn(LockedIn { at })
638                if ts <= at.saturating_add(k.activation_delay) =>
639            {
640                Some(k.version)
641            }
642            _ => None,
643        })
644    }
645
646    /// Retrieve the network version number to announce in block header
647    /// return 0 is there is nothing to announce
648    pub fn get_network_version_to_announce(&self) -> Option<u32> {
649        let lock = self.0.read();
650        let store = lock.deref();
651        // Announce the latest versioning info in Started / LockedIn state
652        // Defined == Not yet ready to announce
653        // Active == current version
654        store.store.iter().rev().find_map(|(k, v)| {
655            matches!(
656                &v.state,
657                &ComponentState::Started(_) | &ComponentState::LockedIn(_)
658            )
659            .then_some(k.version)
660        })
661    }
662
663    pub fn update_network_version_stats(
664        &mut self,
665        slot_timestamp: MassaTime,
666        network_versions: Option<(u32, Option<u32>)>,
667    ) {
668        let mut lock = self.0.write();
669        lock.update_network_version_stats(slot_timestamp, network_versions);
670    }
671
672    #[allow(clippy::result_large_err)]
673    pub fn update_with(
674        &mut self,
675        mip_store: &MipStore,
676    ) -> Result<(Vec<MipInfo>, BTreeMap<MipInfo, MipState>), UpdateWithError> {
677        let mut lock = self.0.write();
678        let lock_other = mip_store.0.read();
679        lock.update_with(lock_other.deref())
680    }
681
682    // Query
683
684    /// Get latest version at given timestamp (e.g. slot) for the given MipComponent.
685    ///
686    /// Considers MIPs whose current store state is `Active` or `LockedIn`: the
687    /// LockedIn→Active transition is a pure function of `(locked_in_at,
688    /// activation_delay, ts)`, so a node still in LockedIn agrees with peers
689    /// that have already advanced to Active on whether `ts` is past activation.
690    /// `state_at(ts)` remains the gate that decides if the version applies at
691    /// `ts` (pre-activation queries while LockedIn still return the previous
692    /// version).
693    pub fn get_latest_component_version_at(&self, component: &MipComponent, ts: MassaTime) -> u32 {
694        let guard = self.0.read();
695        guard.get_latest_component_version_at(component, ts)
696    }
697
698    /// Get all versions in 'Active state' for the given MipComponent
699    pub(crate) fn get_all_active_component_versions(&self, component: &MipComponent) -> Vec<u32> {
700        let guard = self.0.read();
701        guard.get_all_active_component_versions(component)
702    }
703
704    /// Get all versions (at any state) for the given MipComponent
705    pub(crate) fn get_all_component_versions(
706        &self,
707        component: &MipComponent,
708    ) -> BTreeMap<u32, ComponentStateTypeId> {
709        let guard = self.0.read();
710        guard.get_all_component_versions(component)
711    }
712
713    // GRPC
714
715    /// Retrieve a list of MIP info with their corresponding state (as id) - used for grpc API
716    pub fn get_mip_status(&self) -> BTreeMap<MipInfo, ComponentStateTypeId> {
717        let guard = self.0.read();
718        guard
719            .store
720            .iter()
721            .map(|(mip_info, mip_state)| {
722                (
723                    mip_info.clone(),
724                    ComponentStateTypeId::from(&mip_state.state),
725                )
726            })
727            .collect()
728    }
729
730    // Network restart
731    pub fn is_consistent_with_shutdown_period(
732        &self,
733        shutdown_start: Slot,
734        shutdown_end: Slot,
735        thread_count: u8,
736        t0: MassaTime,
737        genesis_timestamp: MassaTime,
738    ) -> Result<(), IsConsistentWithShutdownPeriodError> {
739        let guard = self.0.read();
740        guard.is_consistent_with_shutdown_period(
741            shutdown_start,
742            shutdown_end,
743            thread_count,
744            t0,
745            genesis_timestamp,
746        )
747    }
748
749    #[allow(dead_code)]
750    pub fn update_for_network_shutdown(
751        &mut self,
752        shutdown_start: Slot,
753        shutdown_end: Slot,
754        thread_count: u8,
755        t0: MassaTime,
756        genesis_timestamp: MassaTime,
757    ) -> Result<(), ModelsError> {
758        let mut guard = self.0.write();
759        guard.update_for_network_shutdown(
760            shutdown_start,
761            shutdown_end,
762            thread_count,
763            t0,
764            genesis_timestamp,
765        )
766    }
767
768    pub fn is_key_value_valid(&self, serialized_key: &[u8], serialized_value: &[u8]) -> bool {
769        let guard = self.0.read();
770        guard.is_key_value_valid(serialized_key, serialized_value)
771    }
772
773    // DB
774
775    pub fn update_batches(
776        &self,
777        db_batch: &mut DBBatch,
778        db_versioning_batch: &mut DBBatch,
779        between: Option<(&MassaTime, &MassaTime)>,
780    ) -> Result<(), SerializeError> {
781        let guard = self.0.read();
782        guard.update_batches(db_batch, db_versioning_batch, between)
783    }
784
785    pub fn extend_from_db(
786        &mut self,
787        db: ShareableMassaDBController,
788    ) -> Result<(Vec<MipInfo>, BTreeMap<MipInfo, MipState>), ExtendFromDbError> {
789        let mut guard = self.0.write();
790        guard.extend_from_db(db)
791    }
792
793    /// MIPs of this store that a bootstrap server's streamed state (`db`) does not contain, and
794    /// whose `start` is at or before `at`, the timestamp of the bootstrapped final slot.
795    ///
796    /// Such a server runs a release that predates the MIP, so it cannot provide the MIP's state.
797    /// Accepting it would leave this node replaying the MIP from its compiled-in `Defined` state,
798    /// starting at its own first processed slot: the history it records (and, past lock-in, its
799    /// activation) would differ from the rest of the network's, and once the MIP is Active that
800    /// history is part of the hashed final state. Before `start` nobody has recorded anything for
801    /// the MIP yet, so replaying it locally is deterministic and such a server is still usable.
802    pub fn started_mips_missing_from_db(
803        &self,
804        db: &ShareableMassaDBController,
805        at: MassaTime,
806    ) -> Vec<MipInfo> {
807        let guard = self.0.read();
808        guard.started_mips_missing_from_db(db, at)
809    }
810
811    pub fn reset_db(&self, db: ShareableMassaDBController) {
812        {
813            let mut guard = db.write();
814            guard.delete_prefix(MIP_STORE_PREFIX, STATE_CF, None);
815            guard.delete_prefix(MIP_STORE_PREFIX, VERSIONING_CF, None);
816            guard.delete_prefix(MIP_STORE_STATS_PREFIX, VERSIONING_CF, None);
817        }
818    }
819
820    /// Create a MIP store with what is written on the disk
821    pub fn try_from_db(
822        db: ShareableMassaDBController,
823        cfg: MipStatsConfig,
824    ) -> Result<Self, ExtendFromDbError> {
825        MipStoreRaw::try_from_db(db, cfg).map(|store_raw| Self(Arc::new(RwLock::new(store_raw))))
826    }
827
828    // debug
829    // pub fn len(&self) -> usize {
830    //     let guard = self.0.read();
831    //     guard.store.len()
832    // }
833}
834
835impl<const N: usize> TryFrom<([(MipInfo, MipState); N], MipStatsConfig)> for MipStore {
836    type Error = UpdateWithError;
837
838    fn try_from(
839        (value, cfg): ([(MipInfo, MipState); N], MipStatsConfig),
840    ) -> Result<Self, Self::Error> {
841        MipStoreRaw::try_from((value, cfg)).map(|store_raw| Self(Arc::new(RwLock::new(store_raw))))
842    }
843}
844
845/// Statistics in MipStoreRaw
846#[derive(Debug, Clone, PartialEq)]
847pub struct MipStatsConfig {
848    pub block_count_considered: usize,
849    pub warn_announced_version_ratio: Ratio<u64>,
850}
851
852/// In order for a MIP to be accepted, we compute statistics about other node 'network' version announcement
853#[derive(Debug, Clone, PartialEq)]
854pub(crate) struct MipStoreStats {
855    // config for block count to consider when computing the vote ratio
856    pub(crate) config: MipStatsConfig,
857    // Last network version announcements (in last block header)
858    // Used to clean up the field: network_version_counters (pop the oldest then subtract matching counter)
859    pub(crate) latest_announcements: VecDeque<u32>,
860    // A map where key: network version, value: announcement for this network version count
861    // Note: to avoid various attacks, we have as many counters as version announcements
862    //       + if a counter reset to 0, it is removed from the hash map
863    pub(crate) network_version_counters: HashMap<u32, u64>,
864}
865
866impl MipStoreStats {
867    pub(crate) fn new(config: MipStatsConfig) -> Self {
868        Self {
869            config: config.clone(),
870            latest_announcements: VecDeque::with_capacity(config.block_count_considered),
871            network_version_counters: HashMap::with_capacity(config.block_count_considered),
872        }
873    }
874
875    // reset stats - used in `update_for_network_shutdown` function
876    fn reset(&mut self) {
877        self.latest_announcements.clear();
878        self.network_version_counters.clear();
879    }
880}
881
882/// Error returned by `MipStoreRaw::update_with`
883#[derive(Error, Debug, PartialEq)]
884pub enum UpdateWithError {
885    // State is not consistent with associated MipInfo, ex: State is active but MipInfo.start was not reach yet
886    #[error("MipInfo {0:#?} is not consistent with state: {1:#?}, error: {2}")]
887    NonConsistent(MipInfo, MipState, IsConsistentError),
888    // ex: State is already started but received state is only defined
889    #[error("For MipInfo {0:?}, trying to downgrade from state {1:?} to {2:?}")]
890    Downgrade(MipInfo, ComponentState, ComponentState),
891    // ex: MipInfo 2 start is before MipInfo 1 timeout (MipInfo timings should only be sequential)
892    #[error("MipInfo {0:?} has overlapping data of MipInfo {1:?}")]
893    Overlapping(MipInfo, MipInfo),
894    #[error("MipInfo {0:?} has an invalid activation delay value: {1}, min allowed: {2}")]
895    InvalidActivationDelay(MipInfo, MassaTime, MassaTime),
896}
897
898/// Error returned by 'extend_from_db`
899#[derive(Error, Debug)]
900pub enum ExtendFromDbError {
901    #[error("Unable to get an handle over db column: {0}")]
902    UnknownDbColumn(String),
903    #[error("{0}")]
904    Update(#[from] UpdateWithError),
905    #[error("{0}")]
906    Deserialize(String),
907}
908
909/// Error returned by 'is_consistent_with_shutdown_period`
910#[derive(Error, Debug)]
911pub enum IsConsistentWithShutdownPeriodError {
912    #[error("{0}")]
913    Update(#[from] ModelsError),
914    #[error("MipInfo: {0:?} (state: {1:?}) is not consistent with shutdown: {2} {3}")]
915    NonConsistent(MipInfo, ComponentState, MassaTime, MassaTime),
916}
917
918#[derive(Error, Debug)]
919pub enum IsKVValidError {
920    #[error("{0}")]
921    Deserialize(String),
922    #[error("Invalid prefix for key")]
923    InvalidPrefix,
924}
925
926/// Store of all versioning info
927#[derive(Debug, Clone, PartialEq)]
928pub struct MipStoreRaw {
929    pub(crate) store: BTreeMap<MipInfo, MipState>,
930    pub(crate) stats: MipStoreStats,
931}
932
933impl MipStoreRaw {
934    /// Update our store with another (usually after a bootstrap where we received another store)
935    /// Return list of updated / added if successful, UpdateWithError otherwise
936    #[allow(clippy::result_large_err)]
937    pub fn update_with(
938        &mut self,
939        store_raw: &MipStoreRaw,
940    ) -> Result<(Vec<MipInfo>, BTreeMap<MipInfo, MipState>), UpdateWithError> {
941        // iter over items in given store:
942        // -> 2 cases:
943        // * MipInfo is already in self store -> add to 'to_update' list
944        // * MipInfo is not in self.store -> We received a new MipInfo so we are running an out dated version
945        //                                   of the software
946        //                                   We then return the list of new MipInfo so we can warn and ask
947        //                                   to update the software
948
949        let mut component_versions: HashMap<MipComponent, u32> = self
950            .store
951            .iter()
952            .flat_map(|c| {
953                c.0.components
954                    .iter()
955                    .map(|(mip_component, component_version)| {
956                        (mip_component.clone(), *component_version)
957                    })
958            })
959            .collect();
960        let mut names: HashSet<String> = self.store.keys().map(|mi| mi.name.clone()).collect();
961        let mut to_update: BTreeMap<MipInfo, MipState> = Default::default();
962        let mut to_add: BTreeMap<MipInfo, MipState> = Default::default();
963        let mut has_error: Option<UpdateWithError> = None;
964
965        for (m_info, m_state) in store_raw.store.iter() {
966            if let Err(e) = m_state.is_consistent_with(m_info) {
967                // As soon as we found one non consistent state we abort the merge
968                has_error = Some(UpdateWithError::NonConsistent(
969                    m_info.clone(),
970                    m_state.clone(),
971                    e,
972                ));
973                break;
974            }
975
976            if let Some(m_state_orig) = self.store.get(m_info) {
977                // Given MIP info is already in self
978                // Need to check if we add it to 'to_update' list
979                let m_state_id: u32 = ComponentStateTypeId::from(&m_state.state).into();
980                let m_state_orig_id: u32 = ComponentStateTypeId::from(&m_state_orig.state).into();
981
982                // Note: we do not check for state: active OR failed OR error as they cannot change
983                if matches!(
984                    m_state_orig.state,
985                    ComponentState::Defined(_)
986                        | ComponentState::Started(_)
987                        | ComponentState::LockedIn(_)
988                ) {
989                    // Only accept 'higher' state
990                    // (e.g. 'started' if 'defined', 'locked in' if 'started'...)
991                    if m_state_id >= m_state_orig_id {
992                        to_update.insert(m_info.clone(), m_state.clone());
993                    } else {
994                        // Trying to downgrade state' (e.g. trying to go from 'active' -> 'defined')
995                        has_error = Some(UpdateWithError::Downgrade(
996                            m_info.clone(),
997                            m_state_orig.state,
998                            m_state.state,
999                        ));
1000                        break;
1001                    }
1002                }
1003            } else {
1004                // Given MIP info is not in self
1005                // Need to check if we add it to 'to_add' list
1006
1007                let last_m_info_ = to_add
1008                    .last_key_value()
1009                    .map(|(mi, _)| mi)
1010                    .or(self.store.last_key_value().map(|(mi, _)| mi));
1011
1012                if let Some(last_m_info) = last_m_info_ {
1013                    // check for versions of all components in v_info
1014                    let mut component_version_compatible = true;
1015                    for (component, component_version) in m_info.components.iter() {
1016                        if component_version <= component_versions.get(component).unwrap_or(&0) {
1017                            component_version_compatible = false;
1018                            break;
1019                        }
1020                    }
1021
1022                    #[cfg(not(any(test, feature = "test-exports")))]
1023                    if m_info.activation_delay < VERSIONING_ACTIVATION_DELAY_MIN {
1024                        has_error = Some(UpdateWithError::InvalidActivationDelay(
1025                            m_info.clone(),
1026                            m_info.activation_delay,
1027                            VERSIONING_ACTIVATION_DELAY_MIN,
1028                        ));
1029                        break;
1030                    }
1031
1032                    if m_info.start > last_m_info.timeout
1033                        && m_info.timeout > m_info.start
1034                        && m_info.version > last_m_info.version
1035                        && !names.contains(&m_info.name)
1036                        && component_version_compatible
1037                    {
1038                        // Time range is ok / version is ok / name is unique, let's add it
1039                        to_add.insert(m_info.clone(), m_state.clone());
1040                        names.insert(m_info.name.clone());
1041                        for (component, component_version) in m_info.components.iter() {
1042                            component_versions.insert(component.clone(), *component_version);
1043                        }
1044                    } else {
1045                        // Something is wrong (time range not ok? / version not incr? / names?
1046                        // or component version not incr?)
1047                        has_error = Some(UpdateWithError::Overlapping(
1048                            m_info.clone(),
1049                            last_m_info.clone(),
1050                        ));
1051                        break;
1052                    }
1053                } else {
1054                    // to_add is empty && self.0 is empty
1055                    to_add.insert(m_info.clone(), m_state.clone());
1056                    names.insert(m_info.name.clone());
1057                }
1058            }
1059        }
1060
1061        match has_error {
1062            None => {
1063                let updated: Vec<MipInfo> = to_update.keys().cloned().collect();
1064
1065                // Note: we only update the store with to_update collection
1066                //       having something in the to_add collection means that we need to update
1067                //       the Massa node software
1068                self.store.append(&mut to_update);
1069                Ok((updated, to_add))
1070            }
1071            Some(e) => Err(e),
1072        }
1073    }
1074
1075    pub fn update_network_version_stats(
1076        &mut self,
1077        slot_timestamp: MassaTime,
1078        network_versions: Option<(u32, Option<u32>)>,
1079    ) {
1080        if let Some((_current_network_version, announced_network_version_)) = network_versions {
1081            let announced_network_version = announced_network_version_.unwrap_or(0);
1082
1083            let removed_version_ = match self.stats.latest_announcements.len() {
1084                n if n >= self.stats.config.block_count_considered => {
1085                    self.stats.latest_announcements.pop_front()
1086                }
1087                _ => None,
1088            };
1089            self.stats
1090                .latest_announcements
1091                .push_back(announced_network_version);
1092
1093            // We update the count of the received version (example: update counter for version 1)
1094            let mut network_version_count = *self
1095                .stats
1096                .network_version_counters
1097                .entry(announced_network_version)
1098                .and_modify(|v| *v = v.saturating_add(1))
1099                .or_insert(1);
1100
1101            // If we removed a version announcement, we decrement the corresponding counter
1102            // (example: remove a version 1, so decrement the corresponding counter)
1103            // As soon as a counter value is 0, we remove it
1104            if let Some(removed_version) = removed_version_ {
1105                if let Entry::Occupied(mut e) =
1106                    self.stats.network_version_counters.entry(removed_version)
1107                {
1108                    let entry_value = e.get_mut();
1109                    *entry_value = entry_value.saturating_sub(1);
1110                    network_version_count = *entry_value;
1111                    if *entry_value == 0 {
1112                        self.stats.network_version_counters.remove(&removed_version);
1113                    }
1114                }
1115            }
1116
1117            if announced_network_version != 0 {
1118                let vote_ratio = Ratio::new(
1119                    network_version_count,
1120                    self.stats.config.block_count_considered as u64,
1121                );
1122
1123                if vote_ratio > self.stats.config.warn_announced_version_ratio {
1124                    let last_key_value = self.store.last_key_value();
1125                    if let Some((mi, _ms)) = last_key_value {
1126                        if announced_network_version > mi.version {
1127                            // Vote ratio is > 30%
1128                            // announced version is not known (not in MIP store)
1129                            // announced version is > to the last known network version in MIP store
1130                            // -> Warn the user to update
1131                            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.",
1132                                network_version_count,
1133                                self.stats.config.block_count_considered,
1134                                announced_network_version
1135                            );
1136                        }
1137                    }
1138                }
1139            }
1140        }
1141
1142        debug!(
1143            "[VERSIONING STATS] stats have {} counters and {} announcements",
1144            self.stats.network_version_counters.len(),
1145            self.stats.latest_announcements.len()
1146        );
1147
1148        // Even if stats did not move, update the states (e.g. LockedIn -> Active)
1149        self.advance_states_on_updated_stats(slot_timestamp);
1150    }
1151
1152    /// Used internally by `update_network_version_stats`
1153    fn advance_states_on_updated_stats(&mut self, slot_timestamp: MassaTime) {
1154        for (mi, state) in self.store.iter_mut() {
1155            if state.is_final() {
1156                // State cannot change (ex: Active), no need to update
1157                continue;
1158            }
1159
1160            let network_version_count = *self
1161                .stats
1162                .network_version_counters
1163                .get(&mi.version)
1164                .unwrap_or(&0);
1165
1166            let vote_ratio = Ratio::new(
1167                network_version_count,
1168                self.stats.config.block_count_considered as u64,
1169            );
1170
1171            debug!("[VERSIONING STATS] Vote counts / blocks considered = {} / {} (for MipInfo with network version {} - {})",
1172                network_version_count,
1173                self.stats.config.block_count_considered,
1174                mi.version,
1175                mi.name);
1176
1177            let advance_msg = Advance {
1178                start_timestamp: mi.start,
1179                timeout: mi.timeout,
1180                threshold: vote_ratio,
1181                now: slot_timestamp,
1182                activation_delay: mi.activation_delay,
1183            };
1184
1185            state.on_advance(&advance_msg.clone());
1186        }
1187    }
1188
1189    // Query
1190
1191    /// Get latest version at given timestamp (e.g. slot).
1192    ///
1193    /// Pre-filter includes `LockedIn` as well as `Active`: LockedIn→Active is
1194    /// deterministic given history, so lagging nodes still in LockedIn across
1195    /// the activation instant agree with up-to-date peers on the version at
1196    /// `ts`. See [`MipStore::get_latest_component_version_at`].
1197    fn get_latest_component_version_at(&self, component: &MipComponent, ts: MassaTime) -> u32 {
1198        let version = self
1199            .store
1200            .iter()
1201            .rev()
1202            .filter(|(mi, ms)| {
1203                mi.components.contains_key(component)
1204                    && matches!(
1205                        ms.state,
1206                        ComponentState::Active(_) | ComponentState::LockedIn(_)
1207                    )
1208            })
1209            .find_map(|(mi, ms)| {
1210                let res = ms.state_at(ts, mi.start, mi.timeout, mi.activation_delay);
1211                match res {
1212                    Ok(ComponentStateTypeId::Active) => mi.components.get(component).copied(),
1213                    _ => None,
1214                }
1215            })
1216            .unwrap_or(0);
1217
1218        version
1219    }
1220
1221    /// Get all versions in 'Active state' for the given MipComponent
1222    fn get_all_active_component_versions(&self, component: &MipComponent) -> Vec<u32> {
1223        let versions_iter = self.store.iter().filter_map(|(mi, ms)| {
1224            if matches!(ms.state, ComponentState::Active(_)) {
1225                mi.components.get(component).copied()
1226            } else {
1227                None
1228            }
1229        });
1230        let versions: Vec<u32> = iter::once(0).chain(versions_iter).collect();
1231        versions
1232    }
1233
1234    /// Get all versions (at any state) for the given MipComponent
1235    fn get_all_component_versions(
1236        &self,
1237        component: &MipComponent,
1238    ) -> BTreeMap<u32, ComponentStateTypeId> {
1239        let versions_iter = self.store.iter().filter_map(|(mi, ms)| {
1240            mi.components
1241                .get(component)
1242                .copied()
1243                .map(|component_version| (component_version, ComponentStateTypeId::from(&ms.state)))
1244        });
1245        iter::once((0, ComponentStateTypeId::Active))
1246            .chain(versions_iter)
1247            .collect()
1248    }
1249
1250    // Network restart
1251
1252    /// Check if store is consistent with given last network shutdown
1253    /// On a network shutdown, the MIP infos will be edited but we still need to check if this is consistent
1254    /// See https://github.com/massalabs/massa-functional-tests/blob/main/tests_versioning_network_restart.py
1255    /// for an example
1256    fn is_consistent_with_shutdown_period(
1257        &self,
1258        shutdown_start: Slot,
1259        shutdown_end: Slot,
1260        thread_count: u8,
1261        t0: MassaTime,
1262        genesis_timestamp: MassaTime,
1263    ) -> Result<(), IsConsistentWithShutdownPeriodError> {
1264        // let mut is_consistent = true;
1265        let mut has_error: Result<(), IsConsistentWithShutdownPeriodError> = Ok(());
1266
1267        let shutdown_start_ts =
1268            get_block_slot_timestamp(thread_count, t0, genesis_timestamp, shutdown_start)?;
1269        let shutdown_end_ts =
1270            get_block_slot_timestamp(thread_count, t0, genesis_timestamp, shutdown_end)?;
1271        let shutdown_range = shutdown_start_ts..=shutdown_end_ts;
1272
1273        for (mip_info, mip_state) in &self.store {
1274            match mip_state.state {
1275                ComponentState::Defined(..)
1276                | ComponentState::Started(..)
1277                | ComponentState::LockedIn(..) => {
1278                    // all good if it does not start / timeout during shutdown period
1279                    if shutdown_range.contains(&mip_info.start)
1280                        || shutdown_range.contains(&mip_info.timeout)
1281                    {
1282                        // is_consistent = false;
1283                        has_error = Err(IsConsistentWithShutdownPeriodError::NonConsistent(
1284                            mip_info.clone(),
1285                            mip_state.state,
1286                            shutdown_start_ts,
1287                            shutdown_end_ts,
1288                        ));
1289                        break;
1290                    }
1291                }
1292                _ => {
1293                    // active / failed, error, nothing to do
1294                    // locked in, nothing to do (might go from 'locked in' to 'active' during shutdown)
1295                }
1296            }
1297        }
1298
1299        has_error
1300    }
1301
1302    fn update_for_network_shutdown(
1303        &mut self,
1304        shutdown_start: Slot,
1305        shutdown_end: Slot,
1306        thread_count: u8,
1307        t0: MassaTime,
1308        genesis_timestamp: MassaTime,
1309    ) -> Result<(), ModelsError> {
1310        let shutdown_start_ts =
1311            get_block_slot_timestamp(thread_count, t0, genesis_timestamp, shutdown_start)?;
1312        let shutdown_end_ts =
1313            get_block_slot_timestamp(thread_count, t0, genesis_timestamp, shutdown_end)?;
1314        let shutdown_range = shutdown_start_ts..=shutdown_end_ts;
1315
1316        let mut new_store: BTreeMap<MipInfo, MipState> = Default::default();
1317        let mut new_stats = self.stats.clone();
1318        new_stats.reset();
1319
1320        let next_valid_start_ = shutdown_end.get_next_slot(thread_count)?;
1321        let next_valid_start =
1322            get_block_slot_timestamp(thread_count, t0, genesis_timestamp, next_valid_start_)?;
1323
1324        let mut offset: Option<MassaTime> = None;
1325
1326        for (mip_info, mip_state) in &self.store {
1327            match mip_state.state {
1328                ComponentState::Defined(..) => {
1329                    // Defined: offset start & timeout
1330
1331                    let mut new_mip_info = mip_info.clone();
1332
1333                    if shutdown_range.contains(&new_mip_info.start) {
1334                        let offset_ts = match offset {
1335                            Some(offset_ts) => offset_ts,
1336                            None => {
1337                                let offset_ts = next_valid_start.saturating_sub(mip_info.start);
1338                                offset = Some(offset_ts);
1339                                offset_ts
1340                            }
1341                        };
1342
1343                        new_mip_info.start = new_mip_info.start.saturating_add(offset_ts);
1344                        new_mip_info.timeout = new_mip_info
1345                            .start
1346                            .saturating_add(mip_info.timeout.saturating_sub(mip_info.start));
1347                    }
1348                    new_store.insert(new_mip_info, mip_state.clone());
1349                }
1350                ComponentState::Started(..) | ComponentState::LockedIn(..) => {
1351                    // Started or LockedIn -> Reset to Defined, offset start & timeout
1352
1353                    let mut new_mip_info = mip_info.clone();
1354
1355                    let offset_ts = match offset {
1356                        Some(offset_ts) => offset_ts,
1357                        None => {
1358                            let offset_ts = next_valid_start.saturating_sub(mip_info.start);
1359                            offset = Some(offset_ts);
1360                            offset_ts
1361                        }
1362                    };
1363
1364                    new_mip_info.start = new_mip_info.start.saturating_add(offset_ts);
1365                    new_mip_info.timeout = new_mip_info
1366                        .start
1367                        .saturating_add(mip_info.timeout.saturating_sub(mip_info.start));
1368
1369                    // Need to reset state to 'Defined'
1370                    let new_mip_state = MipState::reset_from(mip_state)
1371                        .ok_or(ModelsError::from("Unable to reset state"))?;
1372                    // Note: statistics are already reset
1373                    new_store.insert(new_mip_info, new_mip_state.clone());
1374                }
1375                _ => {
1376                    // active / failed, error, nothing to do
1377                    new_store.insert(mip_info.clone(), mip_state.clone());
1378                }
1379            }
1380        }
1381
1382        self.store = new_store;
1383        self.stats = new_stats;
1384        Ok(())
1385    }
1386
1387    // get network versions stats
1388    // <network_version, count>
1389    pub fn get_network_versions_stats(&self) -> HashMap<u32, u64> {
1390        self.stats.network_version_counters.clone()
1391    }
1392
1393    // Final state
1394    pub fn is_key_value_valid(&self, serialized_key: &[u8], serialized_value: &[u8]) -> bool {
1395        self._is_key_value_valid(serialized_key, serialized_value)
1396            .is_ok()
1397    }
1398
1399    pub fn _is_key_value_valid(
1400        &self,
1401        serialized_key: &[u8],
1402        serialized_value: &[u8],
1403    ) -> Result<(), IsKVValidError> {
1404        let mip_info_deser = MipInfoDeserializer::new();
1405        let mip_state_deser = MipStateDeserializer::new();
1406        let mip_store_stats_deser = MipStoreStatsDeserializer::new(
1407            MIP_STORE_STATS_BLOCK_CONSIDERED,
1408            self.stats.config.warn_announced_version_ratio,
1409        );
1410
1411        if serialized_key.starts_with(MIP_STORE_PREFIX.as_bytes()) {
1412            let (rem, _mip_info) = mip_info_deser
1413                .deserialize::<DeserializeError>(&serialized_key[MIP_STORE_PREFIX.len()..])
1414                .map_err(|e| IsKVValidError::Deserialize(e.to_string()))?;
1415
1416            if !rem.is_empty() {
1417                return Err(IsKVValidError::Deserialize(
1418                    "Rem not empty after deserialization".to_string(),
1419                ));
1420            }
1421
1422            let (rem2, _mip_state) = mip_state_deser
1423                .deserialize::<DeserializeError>(serialized_value)
1424                .map_err(|e| IsKVValidError::Deserialize(e.to_string()))?;
1425
1426            if !rem2.is_empty() {
1427                return Err(IsKVValidError::Deserialize(
1428                    "Rem not empty after deserialization".to_string(),
1429                ));
1430            }
1431        } else if serialized_key.starts_with(MIP_STORE_STATS_PREFIX.as_bytes()) {
1432            let (rem, _mip_store_stats) = mip_store_stats_deser
1433                .deserialize::<DeserializeError>(serialized_value)
1434                .map_err(|e| IsKVValidError::Deserialize(e.to_string()))?;
1435
1436            if !rem.is_empty() {
1437                return Err(IsKVValidError::Deserialize(
1438                    "Rem not empty after deserialization".to_string(),
1439                ));
1440            }
1441        } else {
1442            return Err(IsKVValidError::InvalidPrefix);
1443        }
1444
1445        Ok(())
1446    }
1447
1448    // DB methods
1449
1450    /// Get MIP store changes between 2 timestamps - used by the db to update the disk
1451    pub fn update_batches(
1452        &self,
1453        batch: &mut DBBatch,
1454        versioning_batch: &mut DBBatch,
1455        between: Option<(&MassaTime, &MassaTime)>,
1456    ) -> Result<(), SerializeError> {
1457        let mip_info_ser = MipInfoSerializer::new();
1458        let mip_state_ser = MipStateSerializer::new();
1459
1460        let bounds = match between {
1461            Some(between) => (*between.0)..(*between.1),
1462            None => MassaTime::from_millis(0)..MassaTime::max(),
1463        };
1464        let mut key = Vec::new();
1465        let mut value = Vec::new();
1466
1467        for (mip_info, mip_state) in self.store.iter() {
1468            if let Some((advance, state_id)) = mip_state.history.last_key_value() {
1469                if bounds.contains(&advance.now) {
1470                    key.extend(MIP_STORE_PREFIX.as_bytes().to_vec());
1471                    mip_info_ser.serialize(mip_info, &mut key)?;
1472                    mip_state_ser.serialize(mip_state, &mut value)?;
1473                    match state_id {
1474                        ComponentStateTypeId::Active => {
1475                            batch.insert(key.clone(), Some(value.clone()));
1476                            // + "Remove key" in VERSIONING_CF
1477                            versioning_batch.insert(key.clone(), None);
1478                        }
1479                        _ => {
1480                            versioning_batch.insert(key.clone(), Some(value.clone()));
1481                        }
1482                    }
1483                    key.clear();
1484                    value.clear();
1485                }
1486            }
1487        }
1488
1489        value.clear();
1490        let mip_stats_ser = MipStoreStatsSerializer::new();
1491        mip_stats_ser.serialize(&self.stats, &mut value)?;
1492        versioning_batch.insert(
1493            MIP_STORE_STATS_PREFIX.as_bytes().to_vec(),
1494            Some(value.clone()),
1495        );
1496
1497        Ok(())
1498    }
1499
1500    /// Extend MIP store with what is written on the disk
1501    fn extend_from_db(
1502        &mut self,
1503        db: ShareableMassaDBController,
1504    ) -> Result<(Vec<MipInfo>, BTreeMap<MipInfo, MipState>), ExtendFromDbError> {
1505        let mip_info_deser = MipInfoDeserializer::new();
1506        let mip_state_deser = MipStateDeserializer::new();
1507        let mip_store_stats_deser = MipStoreStatsDeserializer::new(
1508            MIP_STORE_STATS_BLOCK_CONSIDERED,
1509            self.stats.config.warn_announced_version_ratio,
1510        );
1511
1512        let db = db.read();
1513
1514        // Get data from state cf handle
1515        let mut update_data: BTreeMap<MipInfo, MipState> = Default::default();
1516        for (ser_mip_info, ser_mip_state) in
1517            db.prefix_iterator_cf(STATE_CF, MIP_STORE_PREFIX.as_bytes())
1518        {
1519            if !ser_mip_info.starts_with(MIP_STORE_PREFIX.as_bytes()) {
1520                break;
1521            }
1522
1523            // deser
1524            let (_, mip_info) = mip_info_deser
1525                .deserialize::<DeserializeError>(&ser_mip_info[MIP_STORE_PREFIX.len()..])
1526                .map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
1527
1528            let (_, mip_state) = mip_state_deser
1529                .deserialize::<DeserializeError>(&ser_mip_state)
1530                .map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
1531
1532            update_data.insert(mip_info, mip_state);
1533        }
1534
1535        let (mut updated, mut added) = match update_data.is_empty() {
1536            true => (vec![], BTreeMap::new()),
1537            false => {
1538                let store_raw_ = MipStoreRaw {
1539                    store: update_data,
1540                    stats: MipStoreStats {
1541                        config: MipStatsConfig {
1542                            block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
1543                            warn_announced_version_ratio: self
1544                                .stats
1545                                .config
1546                                .warn_announced_version_ratio,
1547                        },
1548                        latest_announcements: Default::default(),
1549                        network_version_counters: Default::default(),
1550                    },
1551                };
1552                // Only call update_with if update_data is not empty
1553                self.update_with(&store_raw_)?
1554            }
1555        };
1556
1557        let mut update_data: BTreeMap<MipInfo, MipState> = Default::default();
1558
1559        // Get data from state cf handle
1560        for (ser_mip_info, ser_mip_state) in
1561            db.prefix_iterator_cf(VERSIONING_CF, MIP_STORE_PREFIX.as_bytes())
1562        {
1563            // deser
1564
1565            match &ser_mip_info {
1566                key if key.starts_with(MIP_STORE_PREFIX.as_bytes()) => {
1567                    let (_, mip_info) = mip_info_deser
1568                        .deserialize::<DeserializeError>(&ser_mip_info[MIP_STORE_PREFIX.len()..])
1569                        .map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
1570
1571                    let (_, mip_state) = mip_state_deser
1572                        .deserialize::<DeserializeError>(&ser_mip_state)
1573                        .map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
1574
1575                    update_data.insert(mip_info, mip_state);
1576                }
1577                key if key.starts_with(MIP_STORE_STATS_PREFIX.as_bytes()) => {
1578                    let (_, mip_store_stats) = mip_store_stats_deser
1579                        .deserialize::<DeserializeError>(&ser_mip_state)
1580                        .map_err(|e| ExtendFromDbError::Deserialize(e.to_string()))?;
1581
1582                    self.stats = mip_store_stats;
1583                }
1584                _ => {
1585                    break;
1586                }
1587            }
1588        }
1589
1590        if !update_data.is_empty() {
1591            let store_raw_ = MipStoreRaw {
1592                store: update_data,
1593                stats: MipStoreStats {
1594                    config: MipStatsConfig {
1595                        block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
1596                        warn_announced_version_ratio: self
1597                            .stats
1598                            .config
1599                            .warn_announced_version_ratio,
1600                    },
1601                    latest_announcements: Default::default(),
1602                    network_version_counters: Default::default(),
1603                },
1604            };
1605            // Only call update_with if update_data is not empty
1606            let (updated_2, added_2) = self.update_with(&store_raw_)?;
1607            updated.extend(updated_2);
1608            added.extend(added_2);
1609        }
1610
1611        Ok((updated, added))
1612    }
1613
1614    /// See [`MipStore::started_mips_missing_from_db`].
1615    fn started_mips_missing_from_db(
1616        &self,
1617        db: &ShareableMassaDBController,
1618        at: MassaTime,
1619    ) -> Vec<MipInfo> {
1620        let mip_info_deser = MipInfoDeserializer::new();
1621        let db = db.read();
1622
1623        // Active MIPs live in STATE_CF, the others in VERSIONING_CF: the db knows a MIP if
1624        // either holds an entry for it.
1625        let mut in_db: HashSet<String> = HashSet::new();
1626        for cf in [STATE_CF, VERSIONING_CF] {
1627            for (ser_mip_info, _) in db.prefix_iterator_cf(cf, MIP_STORE_PREFIX.as_bytes()) {
1628                if !ser_mip_info.starts_with(MIP_STORE_PREFIX.as_bytes()) {
1629                    break;
1630                }
1631                if let Ok((_, mip_info)) = mip_info_deser
1632                    .deserialize::<DeserializeError>(&ser_mip_info[MIP_STORE_PREFIX.len()..])
1633                {
1634                    in_db.insert(mip_info.name);
1635                }
1636            }
1637        }
1638
1639        self.store
1640            .keys()
1641            .filter(|mip_info| mip_info.start <= at && !in_db.contains(&mip_info.name))
1642            .cloned()
1643            .collect()
1644    }
1645
1646    /// Create a MIP store raw with what is written on the disk
1647    fn try_from_db(
1648        db: ShareableMassaDBController,
1649        cfg: MipStatsConfig,
1650    ) -> Result<Self, ExtendFromDbError> {
1651        let mut store_raw = MipStoreRaw {
1652            store: Default::default(),
1653            stats: MipStoreStats {
1654                config: cfg,
1655                latest_announcements: Default::default(),
1656                network_version_counters: Default::default(),
1657            },
1658        };
1659
1660        let (_updated, mut added) = store_raw.extend_from_db(db)?;
1661        store_raw.store.append(&mut added);
1662        Ok(store_raw)
1663    }
1664}
1665
1666impl<const N: usize> TryFrom<([(MipInfo, MipState); N], MipStatsConfig)> for MipStoreRaw {
1667    type Error = UpdateWithError;
1668
1669    fn try_from(
1670        (value, cfg): ([(MipInfo, MipState); N], MipStatsConfig),
1671    ) -> Result<Self, Self::Error> {
1672        // Build an empty store
1673        let mut store = Self {
1674            store: Default::default(),
1675            stats: MipStoreStats::new(cfg.clone()),
1676        };
1677
1678        // Build another one with given value
1679        let other_store = Self {
1680            store: BTreeMap::from(value),
1681            stats: MipStoreStats::new(cfg),
1682        };
1683
1684        // Use update_with ensuring that we have no overlapping time range, unique names & ...
1685        match store.update_with(&other_store) {
1686            Ok((_updated, mut added)) => {
1687                store.store.append(&mut added);
1688                Ok(store)
1689            }
1690            Err(e) => Err(e),
1691        }
1692    }
1693}
1694
1695// End Store
1696
1697#[cfg(test)]
1698mod test {
1699    use super::*;
1700
1701    use assert_matches::assert_matches;
1702    use massa_db_exports::{MassaDBConfig, MassaDBController, MassaIteratorMode};
1703    use massa_db_worker::MassaDB;
1704    use more_asserts::{assert_gt, assert_le};
1705    use parking_lot::RwLock;
1706    use std::ops::{Add, Sub};
1707    use std::sync::Arc;
1708    use tempfile::tempdir;
1709
1710    use crate::test_helpers::versioning_helpers::advance_state_until;
1711
1712    use massa_models::config::{MIP_STORE_STATS_BLOCK_CONSIDERED, T0, THREAD_COUNT};
1713    use massa_models::timeslots::get_closest_slot_to_timestamp;
1714
1715    // Only for unit tests
1716    impl PartialEq<ComponentState> for MipState {
1717        fn eq(&self, other: &ComponentState) -> bool {
1718            self.state == *other
1719        }
1720    }
1721
1722    // helper
1723    impl From<(&MipInfo, &Ratio<u64>, &MassaTime)> for Advance {
1724        fn from((mip_info, threshold, now): (&MipInfo, &Ratio<u64>, &MassaTime)) -> Self {
1725            Self {
1726                start_timestamp: mip_info.start,
1727                timeout: mip_info.timeout,
1728                threshold: *threshold,
1729                now: *now,
1730                activation_delay: mip_info.activation_delay,
1731            }
1732        }
1733    }
1734
1735    fn get_a_version_info() -> (MassaTime, MassaTime, MipInfo) {
1736        // A helper function to provide a default MipInfo
1737
1738        // Models a Massa Improvements Proposal (MIP-0002), transitioning component address to v2
1739
1740        let start = MassaTime::from_utc_ymd_hms(2017, 11, 1, 7, 33, 44).unwrap();
1741        let timeout = MassaTime::from_utc_ymd_hms(2017, 11, 11, 7, 33, 44).unwrap();
1742
1743        (
1744            start,
1745            timeout,
1746            MipInfo {
1747                name: "MIP-0002".to_string(),
1748                version: 2,
1749                components: BTreeMap::from([(MipComponent::Address, 1)]),
1750                start,
1751                timeout,
1752                activation_delay: MassaTime::from_millis(20),
1753            },
1754        )
1755    }
1756
1757    #[test]
1758    fn test_state_advance_from_defined() {
1759        // Test Versioning state transition (from state: Defined)
1760        let (_, _, mi) = get_a_version_info();
1761        let mut state: ComponentState = Default::default();
1762        assert_eq!(state, ComponentState::defined());
1763
1764        let now = mi.start.saturating_sub(MassaTime::from_millis(1));
1765        let mut advance_msg = Advance::from((&mi, &Ratio::zero(), &now));
1766
1767        state = state.on_advance(advance_msg.clone());
1768        assert_eq!(state, ComponentState::defined());
1769
1770        let now = mi.start.saturating_add(MassaTime::from_millis(5));
1771        advance_msg.now = now;
1772        state = state.on_advance(advance_msg);
1773
1774        // println!("state: {:?}", state);
1775        assert_eq!(
1776            state,
1777            ComponentState::Started(Started {
1778                vote_ratio: Ratio::zero()
1779            })
1780        );
1781    }
1782
1783    #[test]
1784    fn test_state_advance_from_started() {
1785        // Test Versioning state transition (from state: Started)
1786        let (_, _, mi) = get_a_version_info();
1787        let mut state: ComponentState = ComponentState::started(Default::default());
1788
1789        let now = mi.start;
1790        let threshold_too_low =
1791            VERSIONING_THRESHOLD_TRANSITION_ACCEPTED.sub(Ratio::new_raw(10, 100));
1792        let threshold_ok = VERSIONING_THRESHOLD_TRANSITION_ACCEPTED.add(Ratio::new_raw(1, 100));
1793        assert_le!(threshold_ok, Ratio::from_integer(1));
1794        let mut advance_msg = Advance::from((&mi, &threshold_too_low, &now));
1795
1796        state = state.on_advance(advance_msg.clone());
1797        assert_eq!(state, ComponentState::started(threshold_too_low));
1798        advance_msg.threshold = threshold_ok;
1799        state = state.on_advance(advance_msg);
1800        assert_eq!(state, ComponentState::locked_in(now));
1801    }
1802
1803    #[test]
1804    fn test_state_advance_from_locked_in() {
1805        // Test Versioning state transition (from state: LockedIn)
1806        let (_, _, mi) = get_a_version_info();
1807
1808        let locked_in_at = mi.start.saturating_add(MassaTime::from_millis(1));
1809        let mut state: ComponentState = ComponentState::locked_in(locked_in_at);
1810
1811        let now = mi.start;
1812        let mut advance_msg = Advance::from((&mi, &Ratio::zero(), &now));
1813
1814        state = state.on_advance(advance_msg.clone());
1815        assert_eq!(state, ComponentState::locked_in(locked_in_at));
1816
1817        advance_msg.now = advance_msg
1818            .timeout
1819            .saturating_add(MassaTime::from_millis(1));
1820        state = state.on_advance(advance_msg);
1821        assert!(matches!(state, ComponentState::Active(_)));
1822    }
1823
1824    #[test]
1825    fn test_state_advance_from_active() {
1826        // Test Versioning state transition (from state: Active)
1827        let (start, _, mi) = get_a_version_info();
1828        let mut state = ComponentState::active(start);
1829        let now = mi.start;
1830        let advance = Advance::from((&mi, &Ratio::zero(), &now));
1831
1832        state = state.on_advance(advance);
1833        assert!(matches!(state, ComponentState::Active(_)));
1834    }
1835
1836    #[test]
1837    fn test_state_advance_from_failed() {
1838        // Test Versioning state transition (from state: Failed)
1839        let (_, _, mi) = get_a_version_info();
1840        let mut state = ComponentState::failed();
1841        let now = mi.start;
1842        let advance = Advance::from((&mi, &Ratio::zero(), &now));
1843        state = state.on_advance(advance);
1844        assert_eq!(state, ComponentState::failed());
1845    }
1846
1847    #[test]
1848    fn test_state_advance_to_failed() {
1849        // Test Versioning state transition (to state: Failed)
1850        let (_, _, mi) = get_a_version_info();
1851        let now = mi.timeout.saturating_add(MassaTime::from_millis(1));
1852        let advance_msg = Advance::from((&mi, &Ratio::zero(), &now));
1853
1854        let mut state: ComponentState = Default::default(); // Defined
1855        state = state.on_advance(advance_msg.clone());
1856        assert_eq!(state, ComponentState::Failed(Failed {}));
1857
1858        let mut state = ComponentState::started(Default::default());
1859        state = state.on_advance(advance_msg.clone());
1860        assert_eq!(state, ComponentState::Failed(Failed {}));
1861    }
1862
1863    #[test]
1864    fn test_state_with_history() {
1865        // Test MipStateHistory::state_at() function
1866
1867        let (start, _, mi) = get_a_version_info();
1868        let now_0 = start;
1869        let mut state = MipState::new(now_0);
1870
1871        assert_eq!(state, ComponentState::defined());
1872
1873        let now = mi.start.saturating_add(MassaTime::from_millis(15));
1874        let mut advance_msg = Advance::from((&mi, &Ratio::zero(), &now));
1875
1876        // Move from Defined -> Started
1877        state.on_advance(&advance_msg);
1878        assert_eq!(state, ComponentState::started(Ratio::zero()));
1879
1880        // Check history
1881        assert_eq!(state.history.len(), 2);
1882        assert!(matches!(
1883            state.history.first_key_value(),
1884            Some((&AdvanceLW { .. }, &ComponentStateTypeId::Defined))
1885        ));
1886        assert!(matches!(
1887            state.history.last_key_value(),
1888            Some((&AdvanceLW { .. }, &ComponentStateTypeId::Started))
1889        ));
1890
1891        // Query with timestamp
1892
1893        // Before Defined
1894        let state_id_ = state.state_at(
1895            mi.start.saturating_sub(MassaTime::from_millis(5)),
1896            mi.start,
1897            mi.timeout,
1898            mi.activation_delay,
1899        );
1900        assert!(matches!(
1901            state_id_,
1902            Err(StateAtError::BeforeInitialState(_, _))
1903        ));
1904        // After Defined timestamp
1905        let state_id = state
1906            .state_at(mi.start, mi.start, mi.timeout, mi.activation_delay)
1907            .unwrap();
1908        assert_eq!(state_id, ComponentStateTypeId::Defined);
1909        // At Started timestamp
1910        let state_id = state
1911            .state_at(now, mi.start, mi.timeout, mi.activation_delay)
1912            .unwrap();
1913        assert_eq!(state_id, ComponentStateTypeId::Started);
1914
1915        // After Started timestamp but before timeout timestamp
1916        let after_started_ts = now.saturating_add(MassaTime::from_millis(15));
1917        let state_id_ = state.state_at(after_started_ts, mi.start, mi.timeout, mi.activation_delay);
1918        assert_eq!(state_id_, Err(StateAtError::Unpredictable));
1919
1920        // After Started timestamp and after timeout timestamp
1921        let after_timeout_ts = mi.timeout.saturating_add(MassaTime::from_millis(15));
1922        let state_id = state
1923            .state_at(after_timeout_ts, mi.start, mi.timeout, mi.activation_delay)
1924            .unwrap();
1925        assert_eq!(state_id, ComponentStateTypeId::Failed);
1926
1927        // Move from Started to LockedIn
1928        let threshold = VERSIONING_THRESHOLD_TRANSITION_ACCEPTED;
1929        advance_msg.threshold = threshold.add(Ratio::from_integer(1));
1930        advance_msg.now = now.saturating_add(MassaTime::from_millis(1));
1931        state.on_advance(&advance_msg);
1932        assert_eq!(state, ComponentState::locked_in(advance_msg.now));
1933
1934        // Query with timestamp
1935        // After LockedIn timestamp and before timeout timestamp
1936        let after_locked_in_ts = now.saturating_add(MassaTime::from_millis(10));
1937        let state_id = state
1938            .state_at(
1939                after_locked_in_ts,
1940                mi.start,
1941                mi.timeout,
1942                mi.activation_delay,
1943            )
1944            .unwrap();
1945        assert_eq!(state_id, ComponentStateTypeId::LockedIn);
1946        // After LockedIn timestamp and after timeout timestamp
1947        let state_id = state
1948            .state_at(after_timeout_ts, mi.start, mi.timeout, mi.activation_delay)
1949            .unwrap();
1950        assert_eq!(state_id, ComponentStateTypeId::Active);
1951    }
1952
1953    #[test]
1954    fn test_versioning_store_announce_current() {
1955        // Test VersioningInfo::get_version_to_announce() & ::get_version_current()
1956
1957        let (start, timeout, mi) = get_a_version_info();
1958
1959        let mut mi_2 = mi.clone();
1960        mi_2.version += 1;
1961        mi_2.start = timeout
1962            .checked_add(MassaTime::from_millis(1000 * 60 * 60 * 24 * 2))
1963            .unwrap(); // Add 2 days
1964        mi_2.timeout = timeout
1965            .checked_add(MassaTime::from_millis(1000 * 60 * 60 * 24 * 5))
1966            .unwrap(); // Add 5 days
1967
1968        // Can only build such object in test - history is empty :-/
1969        let vs_1 = MipState {
1970            state: ComponentState::active(start),
1971            history: Default::default(),
1972        };
1973        let vs_2 = MipState {
1974            state: ComponentState::started(Ratio::zero()),
1975            history: Default::default(),
1976        };
1977
1978        // TODO: Have VersioningStore::from ?
1979        let mip_stats_cfg = MipStatsConfig {
1980            block_count_considered: 10,
1981            warn_announced_version_ratio: Ratio::new_raw(30, 100),
1982        };
1983        let vs_raw = MipStoreRaw {
1984            store: BTreeMap::from([(mi.clone(), vs_1), (mi_2.clone(), vs_2)]),
1985            stats: MipStoreStats::new(mip_stats_cfg.clone()),
1986        };
1987        // let vs_raw = MipStoreRaw::try_from([(vi.clone(), vs_1), (vi_2.clone(), vs_2)]).unwrap();
1988        let vs = MipStore(Arc::new(RwLock::new(vs_raw)));
1989
1990        assert_eq!(vs.get_network_version_current(), mi.version);
1991        assert_eq!(vs.get_network_version_to_announce(), Some(mi_2.version));
1992
1993        // Test also an empty versioning store
1994        let vs_raw = MipStoreRaw {
1995            store: Default::default(),
1996            stats: MipStoreStats::new(mip_stats_cfg),
1997        };
1998        let vs = MipStore(Arc::new(RwLock::new(vs_raw)));
1999        assert_eq!(vs.get_network_version_current(), 0);
2000        assert_eq!(vs.get_network_version_to_announce(), None);
2001    }
2002
2003    #[test]
2004    fn test_is_consistent_with() {
2005        // Test MipStateHistory::is_consistent_with (consistency of MIP state against its MIP info)
2006
2007        // Given the following MIP info, we expect state
2008        // Defined @ time <= 2
2009        // Started @ time > 2 && <= 5
2010        // LockedIn @ time > time(Started) && <= 5
2011        // Active @time > 5
2012        let vi_1 = MipInfo {
2013            name: "MIP-0002".to_string(),
2014            version: 2,
2015            components: BTreeMap::from([(MipComponent::Address, 1)]),
2016            start: MassaTime::from_millis(2),
2017            timeout: MassaTime::from_millis(5),
2018            activation_delay: MassaTime::from_millis(2),
2019        };
2020        // Another versioning info (from an attacker) for testing
2021        let vi_2 = MipInfo {
2022            name: "MIP-0002".to_string(),
2023            version: 2,
2024            components: BTreeMap::from([(MipComponent::Address, 1)]),
2025            start: MassaTime::from_millis(7),
2026            timeout: MassaTime::from_millis(10),
2027            activation_delay: MassaTime::from_millis(2),
2028        };
2029
2030        let vsh = MipState {
2031            state: ComponentState::Error,
2032            history: Default::default(),
2033        };
2034        // At state Error -> (always) false
2035        assert_eq!(
2036            vsh.is_consistent_with(&vi_1),
2037            Err(IsConsistentError::AtError)
2038        );
2039
2040        let vsh = MipState {
2041            state: ComponentState::defined(),
2042            history: Default::default(),
2043        };
2044        // At state Defined but no history -> false
2045        assert!(vsh.is_consistent_with(&vi_1).is_err());
2046
2047        let mut vsh = MipState::new(MassaTime::from_millis(1));
2048        // At state Defined at time 1 -> true, given vi_1 @ time 1
2049        assert!(vsh.is_consistent_with(&vi_1).is_ok());
2050        // At state Defined at time 1 -> false given vi_1 @ time 3 (state should be Started)
2051        // assert_eq!(vsh.is_consistent_with(&vi_1, MassaTime::from_millis(3)), false);
2052
2053        // Advance to Started
2054        let now = MassaTime::from_millis(3);
2055        let adv = Advance::from((&vi_1, &Ratio::zero(), &now));
2056        vsh.on_advance(&adv);
2057        let now = MassaTime::from_millis(4);
2058        let adv = Advance::from((&vi_1, &Ratio::new_raw(14, 100), &now));
2059        vsh.on_advance(&adv);
2060
2061        // At state Started at time now -> true
2062        assert_eq!(vsh.state, ComponentState::started(Ratio::new_raw(14, 100)));
2063        assert!(vsh.is_consistent_with(&vi_1).is_ok());
2064        // Now with another versioning info
2065        assert!(vsh.is_consistent_with(&vi_2).is_err());
2066
2067        // Advance to LockedIn
2068        let now = MassaTime::from_millis(4);
2069        let adv = Advance::from((&vi_1, &VERSIONING_THRESHOLD_TRANSITION_ACCEPTED, &now));
2070        vsh.on_advance(&adv);
2071
2072        // At state LockedIn at time now -> true
2073        assert_eq!(vsh.state, ComponentState::locked_in(now));
2074        assert!(vsh.is_consistent_with(&vi_1).is_ok());
2075
2076        // edge cases
2077        // TODO: history all good but does not start with Defined, start with Started
2078    }
2079
2080    #[test]
2081    fn test_update_with() {
2082        // Test MipStoreRaw.update_with method (e.g. update a store from another, used in bootstrap)
2083
2084        let vi_1 = MipInfo {
2085            name: "MIP-0002".to_string(),
2086            version: 2,
2087            components: BTreeMap::from([(MipComponent::Address, 1)]),
2088            start: MassaTime::from_millis(2),
2089            timeout: MassaTime::from_millis(5),
2090            activation_delay: MassaTime::from_millis(2),
2091        };
2092
2093        let _time = MassaTime::now();
2094        let vs_1 = advance_state_until(ComponentState::active(_time), &vi_1);
2095        assert!(matches!(vs_1.state, ComponentState::Active(_)));
2096
2097        let vi_2 = MipInfo {
2098            name: "MIP-0003".to_string(),
2099            version: 3,
2100            components: BTreeMap::from([(MipComponent::Address, 2)]),
2101            start: MassaTime::from_millis(17),
2102            timeout: MassaTime::from_millis(27),
2103            activation_delay: MassaTime::from_millis(2),
2104        };
2105        let vs_2 = advance_state_until(ComponentState::defined(), &vi_2);
2106
2107        let mip_stats_cfg = MipStatsConfig {
2108            block_count_considered: 10,
2109            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2110        };
2111        let mut vs_raw_1 = MipStoreRaw::try_from((
2112            [(vi_1.clone(), vs_1.clone()), (vi_2.clone(), vs_2.clone())],
2113            mip_stats_cfg.clone(),
2114        ))
2115        .unwrap();
2116
2117        let vs_2_2 = advance_state_until(ComponentState::active(_time), &vi_2);
2118        assert!(matches!(vs_2_2.state, ComponentState::Active(_)));
2119
2120        let vs_raw_2 = MipStoreRaw::try_from((
2121            [(vi_1.clone(), vs_1.clone()), (vi_2.clone(), vs_2_2.clone())],
2122            mip_stats_cfg,
2123        ))
2124        .unwrap();
2125
2126        let (updated, added) = vs_raw_1.update_with(&vs_raw_2).unwrap();
2127
2128        // Check update_with result
2129        assert!(added.is_empty());
2130        assert_eq!(updated, vec![vi_2.clone()]);
2131
2132        // Expect state 1 (for vi_1) no change, state 2 (for vi_2) updated to "Active"
2133        assert_eq!(vs_raw_1.store.get(&vi_1).unwrap().state, vs_1.state);
2134        assert_eq!(vs_raw_1.store.get(&vi_2).unwrap().state, vs_2_2.state);
2135    }
2136
2137    #[test]
2138    fn test_update_with_invalid() {
2139        // Test updating a MIP store with another invalid one
2140
2141        // part 0 - defines data for the test
2142        let mi_1 = MipInfo {
2143            name: "MIP-0001".to_string(),
2144            version: 2,
2145            components: BTreeMap::from([(MipComponent::Address, 1)]),
2146            start: MassaTime::from_millis(0),
2147            timeout: MassaTime::from_millis(5),
2148            activation_delay: MassaTime::from_millis(2),
2149        };
2150        let _time = MassaTime::now();
2151        let ms_1 = advance_state_until(ComponentState::active(_time), &mi_1);
2152        assert!(matches!(ms_1.state, ComponentState::Active(_)));
2153
2154        let mi_2 = MipInfo {
2155            name: "MIP-0002".to_string(),
2156            version: 3,
2157            components: BTreeMap::from([(MipComponent::Address, 2)]),
2158            start: MassaTime::from_millis(17),
2159            timeout: MassaTime::from_millis(27),
2160            activation_delay: MassaTime::from_millis(2),
2161        };
2162        let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
2163        assert_eq!(ms_2, ComponentState::defined());
2164
2165        let mip_stats_cfg = MipStatsConfig {
2166            block_count_considered: 10,
2167            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2168        };
2169
2170        // case 1: overlapping time range
2171        {
2172            let mut store_1 = MipStoreRaw::try_from((
2173                [(mi_1.clone(), ms_1.clone()), (mi_2.clone(), ms_2.clone())],
2174                mip_stats_cfg.clone(),
2175            ))
2176            .unwrap();
2177
2178            let mut mi_2_2 = mi_2.clone();
2179            // Make mip info invalid (because start == mi_1.timeout)
2180            mi_2_2.start = mi_1.timeout;
2181            let ms_2_2 = advance_state_until(ComponentState::defined(), &mi_2_2);
2182            let store_2 = MipStoreRaw {
2183                store: BTreeMap::from([
2184                    (mi_1.clone(), ms_1.clone()),
2185                    (mi_2_2.clone(), ms_2_2.clone()),
2186                ]),
2187                stats: MipStoreStats::new(mip_stats_cfg.clone()),
2188            };
2189
2190            assert_matches!(
2191                store_1.update_with(&store_2),
2192                Err(UpdateWithError::Overlapping(..))
2193            );
2194            assert_eq!(store_1.store.get(&mi_1).unwrap().state, ms_1.state);
2195            assert_eq!(store_1.store.get(&mi_2).unwrap().state, ms_2.state);
2196
2197            // Check that try_from fails too (because it uses update_with internally)
2198            {
2199                let _store_2_ = MipStoreRaw::try_from((
2200                    [
2201                        (mi_1.clone(), ms_1.clone()),
2202                        (mi_2_2.clone(), ms_2_2.clone()),
2203                    ],
2204                    mip_stats_cfg.clone(),
2205                ));
2206                assert!(_store_2_.is_err());
2207            }
2208        }
2209
2210        // case 2: overlapping versioning component
2211        {
2212            let mut store_1 = MipStoreRaw::try_from((
2213                [(mi_1.clone(), ms_1.clone()), (mi_2.clone(), ms_2.clone())],
2214                mip_stats_cfg.clone(),
2215            ))
2216            .unwrap();
2217
2218            let mut mi_2_2 = mi_2.clone();
2219            // Make MIP invalid (has component version set to 1 - same as MIP 1)
2220            mi_2_2.components = mi_1.components.clone();
2221
2222            let ms_2_2 = advance_state_until(ComponentState::defined(), &mi_2_2);
2223            let store_2 = MipStoreRaw {
2224                store: BTreeMap::from([
2225                    (mi_1.clone(), ms_1.clone()),
2226                    (mi_2_2.clone(), ms_2_2.clone()),
2227                ]),
2228                stats: MipStoreStats::new(mip_stats_cfg.clone()),
2229            };
2230
2231            // MIP-0003 in vs_raw_1 & vs_raw_2 has != components
2232            assert_matches!(
2233                store_1.update_with(&store_2),
2234                Err(UpdateWithError::Overlapping(..))
2235            );
2236        }
2237
2238        // case 3: trying to downgrade network version
2239        {
2240            let mut store_1 =
2241                MipStoreRaw::try_from(([(mi_1.clone(), ms_1.clone())], mip_stats_cfg.clone()))
2242                    .unwrap();
2243            let mut mi_2_2 = mi_2.clone();
2244            // Make MIP 2 invalid (MIP 2 network version < MIP 1 network version)
2245            mi_2_2.version = mi_1.version - 1;
2246
2247            let store_2 = MipStoreRaw {
2248                store: BTreeMap::from([(mi_2_2.clone(), ms_2.clone())]),
2249                stats: MipStoreStats::new(mip_stats_cfg.clone()),
2250            };
2251
2252            assert_matches!(
2253                store_1.update_with(&store_2),
2254                Err(UpdateWithError::Overlapping(..))
2255            );
2256
2257            // Test again but with == network versions
2258
2259            let mut mi_2_3 = mi_2.clone();
2260            // Make MIP 2 invalid (MIP 2 network version == MIP 1 network version)
2261            mi_2_3.version = mi_1.version;
2262
2263            let store_2 = MipStoreRaw {
2264                store: BTreeMap::from([(mi_2_3.clone(), ms_2.clone())]),
2265                stats: MipStoreStats::new(mip_stats_cfg.clone()),
2266            };
2267
2268            assert_matches!(
2269                store_1.update_with(&store_2),
2270                Err(UpdateWithError::Overlapping(..))
2271            );
2272        }
2273
2274        // case 4: non unique name
2275        {
2276            let mut store_1 =
2277                MipStoreRaw::try_from(([(mi_1.clone(), ms_1.clone())], mip_stats_cfg.clone()))
2278                    .unwrap();
2279            let mut mi_2_2 = mi_2.clone();
2280            // Make MIP 2 invalid (MIP 2 name == MIP 1 name)
2281            mi_2_2.name = mi_1.name.clone();
2282
2283            let store_2 = MipStoreRaw {
2284                store: BTreeMap::from([(mi_2_2.clone(), ms_2.clone())]),
2285                stats: MipStoreStats::new(mip_stats_cfg.clone()),
2286            };
2287
2288            assert_matches!(
2289                store_1.update_with(&store_2),
2290                Err(UpdateWithError::Overlapping(..))
2291            );
2292        }
2293
2294        // case 5: trying to downgrade state
2295        {
2296            let ms_1_1 =
2297                advance_state_until(ComponentState::locked_in(MassaTime::from_millis(0)), &mi_1);
2298            let ms_1_2 = advance_state_until(ComponentState::started(Ratio::zero()), &mi_1);
2299            let mut store_1 =
2300                MipStoreRaw::try_from(([(mi_1.clone(), ms_1_1.clone())], mip_stats_cfg.clone()))
2301                    .unwrap();
2302
2303            // Try to downgrade from 'LockedIn' to 'Started'
2304            let store_2 = MipStoreRaw {
2305                store: BTreeMap::from([(mi_1.clone(), ms_1_2.clone())]),
2306                stats: MipStoreStats::new(mip_stats_cfg.clone()),
2307            };
2308
2309            assert_matches!(
2310                store_1.update_with(&store_2),
2311                Err(UpdateWithError::Downgrade(..))
2312            );
2313        }
2314    }
2315
2316    #[test]
2317    fn test_try_from_invalid() {
2318        // Test create a MIP store with invalid MIP info
2319
2320        // part 0 - defines data for the test
2321        let mip_stats_cfg = MipStatsConfig {
2322            block_count_considered: 10,
2323            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2324        };
2325        let mi_1 = MipInfo {
2326            name: "MIP-0002".to_string(),
2327            version: 2,
2328            components: BTreeMap::from([(MipComponent::Address, 1)]),
2329            start: MassaTime::from_millis(0),
2330            timeout: MassaTime::from_millis(5),
2331            activation_delay: MassaTime::from_millis(2),
2332        };
2333        let _time = MassaTime::now();
2334        let ms_1 = advance_state_until(ComponentState::active(_time), &mi_1);
2335        assert!(matches!(ms_1.state, ComponentState::Active(_)));
2336        {
2337            // make mi_1_1 invalid: start is after timeout
2338            let mut mi_1_1 = mi_1.clone();
2339            mi_1_1.start = MassaTime::from_millis(5);
2340            mi_1_1.timeout = MassaTime::from_millis(2);
2341
2342            let mip_store =
2343                MipStoreRaw::try_from(([(mi_1_1, ms_1.clone())], mip_stats_cfg.clone()));
2344            assert_matches!(mip_store, Err(UpdateWithError::NonConsistent(..)));
2345        }
2346        {
2347            let ms_1_2 = MipState::new(MassaTime::from_millis(15));
2348            // make mi_1_1 invalid: start is after timeout
2349            let mut mi_1_2 = mi_1.clone();
2350            mi_1_2.start = MassaTime::from_millis(2);
2351            mi_1_2.timeout = MassaTime::from_millis(5);
2352
2353            let mip_store = MipStoreRaw::try_from(([(mi_1_2, ms_1_2)], mip_stats_cfg.clone()));
2354            assert_matches!(mip_store, Err(UpdateWithError::NonConsistent(..)));
2355        }
2356        {
2357            let ms_1_2 = MipState::new(MassaTime::from_millis(15));
2358            // make mi_1_1 invalid: start is after timeout
2359            let mut mi_1_2 = mi_1.clone();
2360            mi_1_2.start = MassaTime::from_millis(16);
2361            mi_1_2.timeout = MassaTime::from_millis(5);
2362
2363            let mip_store = MipStoreRaw::try_from(([(mi_1_2, ms_1_2)], mip_stats_cfg));
2364            assert_matches!(mip_store, Err(UpdateWithError::NonConsistent(..)));
2365        }
2366    }
2367
2368    #[test]
2369    fn test_empty_mip_store() {
2370        // Test if we can init an empty MipStore
2371
2372        let mip_stats_config = MipStatsConfig {
2373            block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
2374            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2375        };
2376
2377        let mip_store = MipStore::try_from(([], mip_stats_config));
2378        assert!(mip_store.is_ok());
2379    }
2380
2381    #[test]
2382    fn test_update_with_unknown() {
2383        // Test update_with with unknown MipComponent (can happen if a node software is outdated)
2384
2385        // data
2386        let mip_stats_config = MipStatsConfig {
2387            block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
2388            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2389        };
2390
2391        let mut mip_store_raw_1 = MipStoreRaw::try_from(([], mip_stats_config.clone())).unwrap();
2392
2393        let mi_1 = MipInfo {
2394            name: "MIP-0002".to_string(),
2395            version: 2,
2396            components: BTreeMap::from([(MipComponent::__Nonexhaustive, 1)]),
2397            start: MassaTime::from_millis(0),
2398            timeout: MassaTime::from_millis(5),
2399            activation_delay: MassaTime::from_millis(2),
2400        };
2401        let ms_1 = advance_state_until(ComponentState::defined(), &mi_1);
2402        assert_eq!(ms_1, ComponentState::defined());
2403        let mip_store_raw_2 = MipStoreRaw {
2404            store: BTreeMap::from([(mi_1.clone(), ms_1.clone())]),
2405            stats: MipStoreStats::new(mip_stats_config.clone()),
2406        };
2407
2408        let (updated, added) = mip_store_raw_1.update_with(&mip_store_raw_2).unwrap();
2409
2410        assert_eq!(updated.len(), 0);
2411        assert_eq!(added.len(), 1);
2412        assert_eq!(added.get(&mi_1).unwrap().state, ComponentState::defined());
2413    }
2414
2415    #[test]
2416    fn test_mip_store_network_restart() {
2417        // Test if we can get a consistent MipStore after a network shutdown
2418
2419        let genesis_timestamp = MassaTime::from_millis(0);
2420
2421        // helper functions so the test code is easy to read
2422        let get_slot_ts =
2423            |slot| get_block_slot_timestamp(THREAD_COUNT, T0, genesis_timestamp, slot).unwrap();
2424        let is_consistent = |store: &MipStoreRaw, shutdown_start, shutdown_end| {
2425            store.is_consistent_with_shutdown_period(
2426                shutdown_start,
2427                shutdown_end,
2428                THREAD_COUNT,
2429                T0,
2430                genesis_timestamp,
2431            )
2432        };
2433        let update_store = |store: &mut MipStoreRaw, shutdown_start, shutdown_end| {
2434            store
2435                .update_for_network_shutdown(
2436                    shutdown_start,
2437                    shutdown_end,
2438                    THREAD_COUNT,
2439                    T0,
2440                    genesis_timestamp,
2441                )
2442                .unwrap()
2443        };
2444        let _dump_store = |store: &MipStoreRaw| {
2445            println!("Dump store:");
2446            for (mip_info, mip_state) in store.store.iter() {
2447                println!(
2448                    "mip_info {} {} - start: {} - timeout: {}: state: {:?}",
2449                    mip_info.name,
2450                    mip_info.version,
2451                    get_closest_slot_to_timestamp(
2452                        THREAD_COUNT,
2453                        T0,
2454                        genesis_timestamp,
2455                        mip_info.start
2456                    ),
2457                    get_closest_slot_to_timestamp(
2458                        THREAD_COUNT,
2459                        T0,
2460                        genesis_timestamp,
2461                        mip_info.timeout
2462                    ),
2463                    mip_state.state
2464                );
2465            }
2466        };
2467        // end helpers
2468
2469        let shutdown_start = Slot::new(2, 0);
2470        let shutdown_end = Slot::new(8, 0);
2471
2472        let mip_stats_cfg = MipStatsConfig {
2473            block_count_considered: 10,
2474            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2475        };
2476        let mut mi_1 = MipInfo {
2477            name: "MIP-0002".to_string(),
2478            version: 2,
2479            components: BTreeMap::from([(MipComponent::Address, 1)]),
2480            start: MassaTime::from_millis(2),
2481            timeout: MassaTime::from_millis(5),
2482            activation_delay: MassaTime::from_millis(100),
2483        };
2484        let mut mi_2 = MipInfo {
2485            name: "MIP-0003".to_string(),
2486            version: 3,
2487            components: BTreeMap::from([(MipComponent::Address, 2)]),
2488            start: MassaTime::from_millis(7),
2489            timeout: MassaTime::from_millis(11),
2490            activation_delay: MassaTime::from_millis(100),
2491        };
2492
2493        // MipInfo 1 @ state 'Defined' should start during shutdown
2494        {
2495            mi_1.start = get_slot_ts(Slot::new(3, 7));
2496            mi_1.timeout = get_slot_ts(Slot::new(5, 7));
2497            mi_2.start = get_slot_ts(Slot::new(7, 7));
2498            mi_2.timeout = get_slot_ts(Slot::new(10, 7));
2499
2500            let ms_1 = advance_state_until(ComponentState::defined(), &mi_1);
2501            let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
2502            assert_eq!(ms_1, ComponentState::defined());
2503            assert_eq!(ms_2, ComponentState::defined());
2504
2505            let mut store = MipStoreRaw::try_from((
2506                [(mi_1.clone(), ms_1), (mi_2.clone(), ms_2)],
2507                mip_stats_cfg.clone(),
2508            ))
2509            .unwrap();
2510
2511            match is_consistent(&store, shutdown_start, shutdown_end) {
2512                Err(IsConsistentWithShutdownPeriodError::NonConsistent(mi, ..)) => {
2513                    assert_eq!(mi, mi_1);
2514                }
2515                _ => panic!("is_consistent expects a non consistent error"),
2516            }
2517
2518            update_store(&mut store, shutdown_start, shutdown_end);
2519            assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
2520            // _dump_store(&store);
2521        }
2522
2523        // MipInfo 1 @ state 'Defined' will start AFTER shutdown
2524        {
2525            mi_1.start = get_slot_ts(Slot::new(9, 7));
2526            mi_1.timeout = get_slot_ts(Slot::new(11, 7));
2527            mi_2.start = get_slot_ts(Slot::new(12, 7));
2528            mi_2.timeout = get_slot_ts(Slot::new(19, 7));
2529
2530            let ms_1 = advance_state_until(ComponentState::defined(), &mi_1);
2531            let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
2532            assert_eq!(ms_1, ComponentState::defined());
2533            assert_eq!(ms_2, ComponentState::defined());
2534            let mut store = MipStoreRaw::try_from((
2535                [(mi_1.clone(), ms_1), (mi_2.clone(), ms_2)],
2536                mip_stats_cfg.clone(),
2537            ))
2538            .unwrap();
2539            let store_orig = store.clone();
2540
2541            // Already ok even with a shutdown but let's check it
2542            assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
2543            // _dump_store(&store);
2544            update_store(&mut store, shutdown_start, shutdown_end);
2545            assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
2546            // _dump_store(&store);
2547
2548            // Check that nothing has changed
2549            assert_eq!(store_orig, store);
2550        }
2551
2552        // MipInfo 1 @ state 'Started' before shutdown
2553        {
2554            mi_1.start = get_slot_ts(Slot::new(1, 7));
2555            mi_1.timeout = get_slot_ts(Slot::new(5, 7));
2556            mi_2.start = get_slot_ts(Slot::new(7, 7));
2557            mi_2.timeout = get_slot_ts(Slot::new(10, 7));
2558
2559            let ms_1 = advance_state_until(ComponentState::started(Ratio::zero()), &mi_1);
2560            let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
2561            assert_eq!(ms_1, ComponentState::started(Ratio::zero()));
2562            assert_eq!(ms_2, ComponentState::defined());
2563            let mut store = MipStoreRaw::try_from((
2564                [(mi_1.clone(), ms_1), (mi_2.clone(), ms_2)],
2565                mip_stats_cfg.clone(),
2566            ))
2567            .unwrap();
2568
2569            assert_matches!(
2570                is_consistent(&store, shutdown_start, shutdown_end),
2571                Err(IsConsistentWithShutdownPeriodError::NonConsistent(..))
2572            );
2573            update_store(&mut store, shutdown_start, shutdown_end);
2574            assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
2575            // _dump_store(&store);
2576        }
2577
2578        // MipInfo 1 @ state 'Started' after shutdown
2579        {
2580            mi_1.start = get_slot_ts(Slot::new(9, 2));
2581            mi_1.timeout = get_slot_ts(Slot::new(10, 7));
2582            mi_2.start = get_slot_ts(Slot::new(11, 4));
2583            mi_2.timeout = get_slot_ts(Slot::new(12, 0));
2584
2585            let ms_1 = advance_state_until(ComponentState::started(Ratio::zero()), &mi_1);
2586            let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
2587            assert_eq!(ms_1, ComponentState::started(Ratio::zero()));
2588            assert_eq!(ms_2, ComponentState::defined());
2589            let mut store = MipStoreRaw::try_from((
2590                [(mi_1.clone(), ms_1), (mi_2.clone(), ms_2)],
2591                mip_stats_cfg.clone(),
2592            ))
2593            .unwrap();
2594
2595            assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
2596            update_store(&mut store, shutdown_start, shutdown_end);
2597            assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
2598            // _dump_store(&store);
2599        }
2600
2601        // MipInfo 1 @ state 'LockedIn' with transition during shutdown
2602        {
2603            let shutdown_range = shutdown_start..=shutdown_end;
2604
2605            mi_1.start = get_slot_ts(Slot::new(1, 7));
2606            mi_1.timeout = get_slot_ts(Slot::new(5, 7));
2607
2608            // Just before shutdown
2609            let locked_in_at = Slot::new(1, 9);
2610            assert!(locked_in_at < shutdown_start);
2611            let activate_at = Slot::new(4, 0);
2612            assert!(shutdown_range.contains(&activate_at));
2613            // MIP 1 in state 'LockedIn', should transition to 'Active' during shutdown period
2614            mi_1.activation_delay =
2615                get_slot_ts(activate_at).saturating_sub(get_slot_ts(locked_in_at));
2616            let ms_1 = advance_state_until(
2617                ComponentState::locked_in(get_slot_ts(Slot::new(1, 9))),
2618                &mi_1,
2619            );
2620
2621            // MIP 2 in state 'Defined'
2622            mi_2.start = get_slot_ts(Slot::new(7, 7));
2623            mi_2.timeout = get_slot_ts(Slot::new(10, 7));
2624            let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
2625            let mut store = MipStoreRaw::try_from((
2626                [(mi_1.clone(), ms_1), (mi_2.clone(), ms_2)],
2627                mip_stats_cfg.clone(),
2628            ))
2629            .unwrap();
2630
2631            match is_consistent(&store, shutdown_start, shutdown_end) {
2632                Err(IsConsistentWithShutdownPeriodError::NonConsistent(mi, ..)) => {
2633                    assert_eq!(mi, mi_1);
2634                }
2635                _ => panic!("is_consistent expects a non consistent error"),
2636            }
2637            // _dump_store(&store);
2638            update_store(&mut store, shutdown_start, shutdown_end);
2639            assert!(is_consistent(&store, shutdown_start, shutdown_end).is_ok());
2640            // _dump_store(&store);
2641
2642            // Update stats - so should force transitions if any
2643            store.update_network_version_stats(
2644                get_slot_ts(shutdown_end.get_next_slot(THREAD_COUNT).unwrap()),
2645                Some((1, None)),
2646            );
2647
2648            let (first_mi_info, first_mi_state) = store.store.first_key_value().unwrap();
2649            assert_eq!(*first_mi_info.name, mi_1.name);
2650            // State was 'LockedIn' -> reset, start ts now defined right after network restart
2651            assert_eq!(
2652                ComponentStateTypeId::from(&first_mi_state.state),
2653                ComponentStateTypeId::Started
2654            );
2655            let (last_mi_info, last_mi_state) = store.store.last_key_value().unwrap();
2656            assert_eq!(*last_mi_info.name, mi_2.name);
2657            // State was 'Defined' -> start is set up after MIP 1 start & timeout
2658            assert_eq!(
2659                ComponentStateTypeId::from(&last_mi_state.state),
2660                ComponentStateTypeId::Defined
2661            );
2662        }
2663    }
2664
2665    #[test]
2666    fn test_started_mips_missing_from_db() {
2667        // A bootstrap server on an older release streams a MIP store without the newest MIP.
2668        // The client must flag that MIP once its vote has started by the bootstrapped slot,
2669        // and only then: before `start`, replaying it locally is deterministic.
2670
2671        let genesis_timestamp = MassaTime::from_millis(0);
2672        let get_slot_ts =
2673            |slot| get_block_slot_timestamp(THREAD_COUNT, T0, genesis_timestamp, slot).unwrap();
2674
2675        let temp_dir = tempdir().expect("Unable to create a temp folder");
2676        let db_config = MassaDBConfig {
2677            path: temp_dir.path().to_path_buf(),
2678            max_history_length: 100,
2679            max_final_state_elements_size: 100_000,
2680            max_versioning_elements_size: 100_000,
2681            thread_count: THREAD_COUNT,
2682            max_ledger_backups: 10,
2683            enable_metrics: false,
2684        };
2685        let db = Arc::new(RwLock::new(
2686            Box::new(MassaDB::new(db_config)) as Box<dyn MassaDBController + 'static>
2687        ));
2688
2689        // Known to both releases, Active: streamed through STATE_CF.
2690        let mi_old = MipInfo {
2691            name: "MIP-0002".to_string(),
2692            version: 2,
2693            components: BTreeMap::from([(MipComponent::Address, 1)]),
2694            start: get_slot_ts(Slot::new(2, 0)),
2695            timeout: get_slot_ts(Slot::new(3, 0)),
2696            activation_delay: MassaTime::from_millis(10),
2697        };
2698        let ms_old = advance_state_until(
2699            ComponentState::active(get_slot_ts(Slot::new(2, 5))),
2700            &mi_old,
2701        );
2702        // Known only to the newer (client) release.
2703        let mi_new = MipInfo {
2704            name: "MIP-0003".to_string(),
2705            version: 3,
2706            components: BTreeMap::from([(MipComponent::Address, 2)]),
2707            start: get_slot_ts(Slot::new(4, 2)),
2708            timeout: get_slot_ts(Slot::new(7, 2)),
2709            activation_delay: MassaTime::from_millis(10),
2710        };
2711        let ms_new = advance_state_until(ComponentState::defined(), &mi_new);
2712
2713        let mip_stats_config = MipStatsConfig {
2714            block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
2715            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2716        };
2717
2718        // What the old server streams: its own store, which never heard of MIP-0003.
2719        let server_store =
2720            MipStore::try_from(([(mi_old.clone(), ms_old)], mip_stats_config.clone()))
2721                .expect("Cannot create the server MIP store");
2722        let mut db_batch = DBBatch::new();
2723        let mut db_versioning_batch = DBBatch::new();
2724        server_store
2725            .update_batches(&mut db_batch, &mut db_versioning_batch, None)
2726            .unwrap();
2727        db.write()
2728            .write_batch(db_batch, db_versioning_batch, Some(Slot::new(3, 0)));
2729
2730        let client_store = MipStore::try_from((
2731            [
2732                (
2733                    mi_old.clone(),
2734                    advance_state_until(ComponentState::defined(), &mi_old),
2735                ),
2736                (mi_new.clone(), ms_new),
2737            ],
2738            mip_stats_config,
2739        ))
2740        .expect("Cannot create the client MIP store");
2741
2742        // Bootstrapped before MIP-0003 starts: nothing recorded yet anywhere, fine.
2743        let before_start = get_slot_ts(Slot::new(4, 1));
2744        assert!(client_store
2745            .started_mips_missing_from_db(&db, before_start)
2746            .is_empty());
2747
2748        // Bootstrapped at or after its start: the server cannot provide its state.
2749        for at in [mi_new.start, get_slot_ts(Slot::new(5, 0))] {
2750            assert_eq!(
2751                client_store.started_mips_missing_from_db(&db, at),
2752                vec![mi_new.clone()]
2753            );
2754        }
2755
2756        // A MIP the server does stream is never reported, however late the bootstrap.
2757        assert!(!client_store
2758            .started_mips_missing_from_db(&db, get_slot_ts(Slot::new(9, 0)))
2759            .contains(&mi_old));
2760    }
2761
2762    #[test]
2763    fn test_mip_store_db() {
2764        // Test interaction of MIP store with MassaDB
2765        // 1- init from db (empty disk)
2766        // 2- update state
2767        // 3- write changes to db
2768        // 4- init a new mip store from disk and compare
2769        // 5- test is_key_value_valid method
2770
2771        let genesis_timestamp = MassaTime::from_millis(0);
2772        // helpers
2773        let get_slot_ts =
2774            |slot| get_block_slot_timestamp(THREAD_COUNT, T0, genesis_timestamp, slot).unwrap();
2775
2776        // Db init
2777
2778        let temp_dir = tempdir().expect("Unable to create a temp folder");
2779        // println!("Using temp dir: {:?}", temp_dir.path());
2780
2781        let db_config = MassaDBConfig {
2782            path: temp_dir.path().to_path_buf(),
2783            max_history_length: 100,
2784            max_final_state_elements_size: 100_000,
2785            max_versioning_elements_size: 100_000,
2786            thread_count: THREAD_COUNT,
2787            max_ledger_backups: 10,
2788            enable_metrics: false,
2789        };
2790        let db = Arc::new(RwLock::new(
2791            Box::new(MassaDB::new(db_config)) as Box<dyn MassaDBController + 'static>
2792        ));
2793
2794        // MIP info / store init
2795
2796        let mi_1 = MipInfo {
2797            name: "MIP-0002".to_string(),
2798            version: 2,
2799            components: BTreeMap::from([(MipComponent::Address, 1)]),
2800            start: get_slot_ts(Slot::new(2, 0)),
2801            timeout: get_slot_ts(Slot::new(3, 0)),
2802            activation_delay: MassaTime::from_millis(10),
2803        };
2804        let ms_1 = advance_state_until(ComponentState::defined(), &mi_1);
2805
2806        let mi_2 = MipInfo {
2807            name: "MIP-0003".to_string(),
2808            version: 3,
2809            components: BTreeMap::from([(MipComponent::Address, 2)]),
2810            start: get_slot_ts(Slot::new(4, 2)),
2811            timeout: get_slot_ts(Slot::new(7, 2)),
2812            activation_delay: MassaTime::from_millis(10),
2813        };
2814        let ms_2 = advance_state_until(ComponentState::defined(), &mi_2);
2815
2816        let mip_stats_config = MipStatsConfig {
2817            block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
2818            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2819        };
2820        let mut mip_store = MipStore::try_from((
2821            [(mi_1.clone(), ms_1.clone()), (mi_2.clone(), ms_2.clone())],
2822            mip_stats_config.clone(),
2823        ))
2824        .expect("Cannot create an empty MIP store");
2825
2826        // Step 1
2827
2828        mip_store.extend_from_db(db.clone()).unwrap();
2829        // Check that we extend from an empty folder
2830        assert_eq!(mip_store.0.read().store.len(), 2);
2831        assert_eq!(
2832            mip_store.0.read().store.first_key_value(),
2833            Some((&mi_1, &ms_1))
2834        );
2835        assert_eq!(
2836            mip_store.0.read().store.last_key_value(),
2837            Some((&mi_2, &ms_2))
2838        );
2839
2840        // Step 2
2841        let active_at = get_slot_ts(Slot::new(2, 5));
2842        let ms_1_ = advance_state_until(ComponentState::active(active_at), &mi_1);
2843
2844        let mip_store_ =
2845            MipStore::try_from(([(mi_1.clone(), ms_1_.clone())], mip_stats_config.clone()))
2846                .expect("Cannot create an empty MIP store");
2847
2848        let (updated, added) = mip_store.update_with(&mip_store_).unwrap();
2849
2850        // Check update_with result - only 1 state should be updated
2851        assert_eq!(updated.len(), 1);
2852        assert_eq!(added.len(), 0);
2853        assert_eq!(mip_store.0.read().store.len(), 2);
2854        assert_eq!(
2855            mip_store.0.read().store.first_key_value(),
2856            Some((&mi_1, &ms_1_))
2857        );
2858        assert_eq!(
2859            mip_store.0.read().store.last_key_value(),
2860            Some((&mi_2, &ms_2))
2861        );
2862
2863        // Step 3
2864
2865        let mut db_batch = DBBatch::new();
2866        let mut db_versioning_batch = DBBatch::new();
2867
2868        // FIXME: get slot right after active at - no hardcode
2869        let slot_bounds_ = (&Slot::new(1, 0), &Slot::new(4, 2));
2870        let between = (&get_slot_ts(*slot_bounds_.0), &get_slot_ts(*slot_bounds_.1));
2871
2872        mip_store
2873            .update_batches(&mut db_batch, &mut db_versioning_batch, Some(between))
2874            .unwrap();
2875
2876        assert_eq!(db_batch.len(), 1); // mi_1
2877        assert_eq!(db_versioning_batch.len(), 3); // mi_2 + mi_1 removal + stats
2878
2879        let mut guard_db = db.write();
2880        // FIXME / TODO: no slot hardcoding?
2881        guard_db.write_batch(db_batch, db_versioning_batch, Some(Slot::new(3, 0)));
2882        drop(guard_db);
2883
2884        // Step 4
2885        let mut mip_store_2 = MipStore::try_from((
2886            [(mi_1.clone(), ms_1.clone()), (mi_2.clone(), ms_2.clone())],
2887            mip_stats_config.clone(),
2888        ))
2889        .expect("Cannot create an empty MIP store");
2890        // assert_eq!(mip_store_2.0.read().store.len(), 0);
2891
2892        mip_store_2.extend_from_db(db.clone()).unwrap();
2893
2894        let guard_1 = mip_store.0.read();
2895        let guard_2 = mip_store_2.0.read();
2896        let st1_raw = guard_1.deref();
2897        let st2_raw = guard_2.deref();
2898
2899        // println!("st1_raw: {:?}", st1_raw);
2900        // println!("st2_raw: {:?}", st2_raw);
2901        assert_eq!(st1_raw, st2_raw);
2902
2903        // Step 5: test is_key_value_valid
2904        let mut count = 0;
2905        for (ser_key, ser_value) in db
2906            .read()
2907            .iterator_cf_for_full_db_traversal(STATE_CF, MassaIteratorMode::Start)
2908        {
2909            assert!(mip_store.is_key_value_valid(&ser_key, &ser_value));
2910            count += 1;
2911        }
2912        assert_gt!(count, 0);
2913
2914        let mut count2 = 0;
2915        for (ser_key, ser_value) in db
2916            .read()
2917            .iterator_cf_for_full_db_traversal(VERSIONING_CF, MassaIteratorMode::Start)
2918        {
2919            assert!(mip_store.is_key_value_valid(&ser_key, &ser_value));
2920            count2 += 1;
2921        }
2922        assert_gt!(count2, 0);
2923    }
2924
2925    #[test]
2926    fn test_mip_store_stats() {
2927        // Test MipStoreRaw stats
2928
2929        // helper functions so the test code is easy to read
2930        let genesis_timestamp = MassaTime::from_millis(0);
2931        let get_slot_ts =
2932            |slot| get_block_slot_timestamp(THREAD_COUNT, T0, genesis_timestamp, slot).unwrap();
2933
2934        let mip_stats_config = MipStatsConfig {
2935            block_count_considered: 2,
2936            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2937        };
2938        let activation_delay = MassaTime::from_millis(100);
2939        let timeout = MassaTime::now().saturating_add(MassaTime::from_millis(50_000)); // + 50 seconds
2940        let mi_1 = MipInfo {
2941            name: "MIP-0001".to_string(),
2942            version: 1,
2943            components: BTreeMap::from([(MipComponent::Address, 1)]),
2944            start: MassaTime::from_millis(2),
2945            timeout,
2946            activation_delay,
2947        };
2948        let ms_1 = advance_state_until(ComponentState::started(Ratio::zero()), &mi_1);
2949
2950        let mut mip_store =
2951            MipStoreRaw::try_from(([(mi_1.clone(), ms_1)], mip_stats_config)).unwrap();
2952
2953        // Current network version is 0, next one is 1
2954        mip_store.update_network_version_stats(get_slot_ts(Slot::new(1, 0)), Some((0, Some(1))));
2955        assert_eq!(mip_store.stats.network_version_counters.len(), 1);
2956        assert_eq!(mip_store.stats.network_version_counters.get(&1), Some(&1));
2957
2958        mip_store.update_network_version_stats(get_slot_ts(Slot::new(1, 0)), Some((0, Some(1))));
2959        assert_eq!(mip_store.stats.network_version_counters.len(), 1);
2960        assert_eq!(mip_store.stats.network_version_counters.get(&1), Some(&2));
2961
2962        // Check that MipInfo is now
2963        let (mi_, ms_) = mip_store.store.last_key_value().unwrap();
2964        assert_eq!(*mi_, mi_1);
2965        assert_matches!(ms_.state, ComponentState::LockedIn(..));
2966
2967        let mut at = MassaTime::now();
2968        at = at.saturating_add(activation_delay);
2969        assert_eq!(
2970            ms_.state_at(at, mi_1.start, mi_1.timeout, mi_1.activation_delay),
2971            Ok(ComponentStateTypeId::Active)
2972        );
2973
2974        // Now network version is 1, next one is 2
2975        mip_store.update_network_version_stats(get_slot_ts(Slot::new(1, 0)), Some((1, Some(2))));
2976        // Counter for announced version: 1 & 2
2977        assert_eq!(mip_store.stats.network_version_counters.len(), 2);
2978        // First announced version 1 was removed and so the counter decremented
2979        assert_eq!(mip_store.stats.network_version_counters.get(&1), Some(&1));
2980        assert_eq!(mip_store.stats.network_version_counters.get(&2), Some(&1));
2981    }
2982
2983    /// A node still in LockedIn must agree with an Active peer on the *network*
2984    /// version of a slot past the activation instant: block headers are stamped
2985    /// and checked with it, and a mismatch makes one side reject the other's blocks.
2986    #[test]
2987    fn test_get_network_version_active_at_locked_in_matches_active() {
2988        let mi = MipInfo {
2989            name: "MIP-test-LockedIn-net".to_string(),
2990            version: 2,
2991            components: BTreeMap::from([(MipComponent::Execution, 2)]),
2992            start: MassaTime::from_millis(10_000),
2993            timeout: MassaTime::from_millis(100_000),
2994            activation_delay: MassaTime::from_millis(5_000),
2995        };
2996        let stats = MipStatsConfig {
2997            block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
2998            warn_announced_version_ratio: Ratio::new_raw(30, 100),
2999        };
3000        let store_locked = MipStore::try_from((
3001            [(
3002                mi.clone(),
3003                advance_state_until(ComponentState::locked_in(MassaTime::from_millis(0)), &mi),
3004            )],
3005            stats.clone(),
3006        ))
3007        .expect("locked store");
3008        let store_active = MipStore::try_from((
3009            [(
3010                mi.clone(),
3011                advance_state_until(ComponentState::active(MassaTime::from_millis(0)), &mi),
3012            )],
3013            stats,
3014        ))
3015        .expect("active store");
3016
3017        let locked_in_at = match store_locked.0.read().store.get(&mi).unwrap().state {
3018            ComponentState::LockedIn(LockedIn { at }) => at,
3019            other => panic!("expected LockedIn, got {:?}", other),
3020        };
3021        let instant = locked_in_at.saturating_add(mi.activation_delay);
3022        let after = instant.saturating_add(MassaTime::from_millis(1));
3023
3024        // Up to and including the instant: not yet active, still announced.
3025        for ts in [locked_in_at, instant] {
3026            assert_eq!(store_locked.get_network_version_active_at(ts), 0);
3027            assert_eq!(store_active.get_network_version_active_at(ts), 0);
3028            assert_eq!(store_locked.get_network_version_to_announce_at(ts), Some(2));
3029        }
3030        // Past it: active for both, and no longer announced.
3031        assert_eq!(store_locked.get_network_version_active_at(after), 2);
3032        assert_eq!(store_active.get_network_version_active_at(after), 2);
3033        assert_eq!(store_locked.get_network_version_to_announce_at(after), None);
3034        assert_eq!(store_active.get_network_version_to_announce_at(after), None);
3035
3036        // The store-state based answers still lag until the store itself flips.
3037        assert_eq!(store_locked.get_network_version_current(), 0);
3038        assert_eq!(store_locked.get_network_version_to_announce(), Some(2));
3039    }
3040
3041    /// Every header built from `get_network_version_active_at` and
3042    /// `get_network_version_to_announce_at` at the same timestamp must pass the
3043    /// protocol check `announced > current`, whatever state the store is in.
3044    #[test]
3045    fn test_header_versions_at_are_always_consistent() {
3046        let mi = MipInfo {
3047            name: "MIP-test-header".to_string(),
3048            version: 2,
3049            components: BTreeMap::from([(MipComponent::Execution, 2)]),
3050            start: MassaTime::from_millis(10_000),
3051            timeout: MassaTime::from_millis(100_000),
3052            activation_delay: MassaTime::from_millis(5_000),
3053        };
3054        let stats = MipStatsConfig {
3055            block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
3056            warn_announced_version_ratio: Ratio::new_raw(30, 100),
3057        };
3058        for state in [
3059            ComponentState::started(Ratio::zero()),
3060            ComponentState::locked_in(MassaTime::from_millis(0)),
3061            ComponentState::active(MassaTime::from_millis(0)),
3062        ] {
3063            let store = MipStore::try_from((
3064                [(mi.clone(), advance_state_until(state, &mi))],
3065                stats.clone(),
3066            ))
3067            .expect("store");
3068            for ts in (0..40_000u64).step_by(500).map(MassaTime::from_millis) {
3069                let current = store.get_network_version_active_at(ts);
3070                if let Some(announced) = store.get_network_version_to_announce_at(ts) {
3071                    assert!(
3072                        announced > current,
3073                        "ts {}: announced {} <= current {}",
3074                        ts.as_millis(),
3075                        announced,
3076                        current
3077                    );
3078                }
3079            }
3080        }
3081    }
3082
3083    /// A node still in LockedIn must agree with an Active peer on the component
3084    /// version at a post-activation timestamp (activation-boundary determinism).
3085    #[test]
3086    fn test_get_latest_component_version_at_locked_in_matches_active() {
3087        let start = MassaTime::from_millis(10_000);
3088        let timeout = MassaTime::from_millis(100_000);
3089        let activation_delay = MassaTime::from_millis(5_000);
3090        let mi = MipInfo {
3091            name: "MIP-test-LockedIn".to_string(),
3092            version: 2,
3093            components: BTreeMap::from([(MipComponent::Execution, 2)]),
3094            start,
3095            timeout,
3096            activation_delay,
3097        };
3098        let stats = MipStatsConfig {
3099            block_count_considered: MIP_STORE_STATS_BLOCK_CONSIDERED,
3100            warn_announced_version_ratio: Ratio::new_raw(30, 100),
3101        };
3102
3103        let ms_locked =
3104            advance_state_until(ComponentState::locked_in(MassaTime::from_millis(0)), &mi);
3105        assert_matches!(ms_locked.state, ComponentState::LockedIn(_));
3106        let store_locked =
3107            MipStore::try_from(([(mi.clone(), ms_locked)], stats.clone())).expect("locked store");
3108
3109        let ms_active = advance_state_until(ComponentState::active(MassaTime::from_millis(0)), &mi);
3110        assert_matches!(ms_active.state, ComponentState::Active(_));
3111        let store_active =
3112            MipStore::try_from(([(mi.clone(), ms_active)], stats)).expect("active store");
3113
3114        let locked_in_at = match store_locked.0.read().store.get(&mi).unwrap().state {
3115            ComponentState::LockedIn(LockedIn { at }) => at,
3116            other => panic!("expected LockedIn, got {:?}", other),
3117        };
3118        let before = locked_in_at
3119            .saturating_add(activation_delay)
3120            .saturating_sub(MassaTime::from_millis(1));
3121        // LockedIn::on_advance activates only when `now > locked_in_at + delay`
3122        let after = locked_in_at
3123            .saturating_add(activation_delay)
3124            .saturating_add(MassaTime::from_millis(1));
3125
3126        assert_eq!(
3127            store_locked.get_latest_component_version_at(&MipComponent::Execution, before),
3128            0
3129        );
3130        assert_eq!(
3131            store_active.get_latest_component_version_at(&MipComponent::Execution, before),
3132            0
3133        );
3134        assert_eq!(
3135            store_locked.get_latest_component_version_at(&MipComponent::Execution, after),
3136            2
3137        );
3138        assert_eq!(
3139            store_active.get_latest_component_version_at(&MipComponent::Execution, after),
3140            2
3141        );
3142    }
3143}