nym_mixnet_contract_common/
interval.rs

1// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::NodeId;
5use crate::error::MixnetContractError;
6use crate::nym_node::Role;
7use cosmwasm_schema::cw_serde;
8use cosmwasm_schema::schemars::JsonSchema;
9use cosmwasm_schema::schemars::r#gen::SchemaGenerator;
10use cosmwasm_schema::schemars::schema::{InstanceType, Schema, SchemaObject};
11use cosmwasm_std::{Addr, Env};
12use serde::{Deserialize, Serialize};
13use std::fmt::{Display, Formatter};
14use std::time::Duration;
15use time::OffsetDateTime;
16
17pub type EpochId = u32;
18pub type IntervalId = u32;
19
20// internally, since version 0.3.6, time uses deserialize_any for deserialization, which can't be handled
21// by serde wasm. We could just downgrade to 0.3.5 and call it a day, but then it would break
22// when we decided to upgrade it at some point in the future. And then it would have been more problematic
23// to fix it, since the data would have already been stored inside the contract.
24// Hence, an explicit workaround to use string representation of Rfc3339-formatted datetime.
25pub(crate) mod string_rfc3339_offset_date_time {
26    use serde::de::Visitor;
27    use serde::ser::Error;
28    use serde::{Deserializer, Serialize, Serializer};
29    use std::fmt::Formatter;
30    use time::OffsetDateTime;
31    use time::format_description::well_known::Rfc3339;
32
33    struct Rfc3339OffsetDateTimeVisitor;
34
35    impl Visitor<'_> for Rfc3339OffsetDateTimeVisitor {
36        type Value = OffsetDateTime;
37
38        fn expecting(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
39            formatter.write_str("an rfc3339 `OffsetDateTime`")
40        }
41
42        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
43        where
44            E: serde::de::Error,
45        {
46            OffsetDateTime::parse(value, &Rfc3339).map_err(E::custom)
47        }
48    }
49
50    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
51    where
52        D: Deserializer<'de>,
53    {
54        deserializer.deserialize_str(Rfc3339OffsetDateTimeVisitor)
55    }
56
57    pub(crate) fn serialize<S>(datetime: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
58    where
59        S: Serializer,
60    {
61        datetime
62            .format(&Rfc3339)
63            .map_err(S::Error::custom)?
64            .serialize(serializer)
65    }
66}
67
68/// The status of the current rewarding epoch.
69#[cw_serde]
70pub struct EpochStatus {
71    // TODO: introduce mechanism to allow another validator to take over if no progress has been made in X blocks / Y seconds
72    /// Specifies either, which validator is currently performing progression into the following epoch (if the epoch is currently being progressed),
73    /// or which validator was responsible for progressing into the current epoch (if the epoch is currently in progress)
74    pub being_advanced_by: Addr,
75
76    /// The concrete state of the epoch.
77    pub state: EpochState,
78}
79
80impl EpochStatus {
81    pub fn new(being_advanced_by: Addr) -> Self {
82        EpochStatus {
83            being_advanced_by,
84            state: EpochState::InProgress,
85        }
86    }
87
88    pub fn update_last_rewarded(
89        &mut self,
90        new_last_rewarded: NodeId,
91    ) -> Result<bool, MixnetContractError> {
92        match &mut self.state {
93            EpochState::Rewarding {
94                last_rewarded,
95                final_node_id,
96            } => {
97                if new_last_rewarded <= *last_rewarded {
98                    return Err(MixnetContractError::RewardingOutOfOrder {
99                        last_rewarded: *last_rewarded,
100                        attempted_to_reward: new_last_rewarded,
101                    });
102                }
103
104                *last_rewarded = new_last_rewarded;
105                Ok(new_last_rewarded == *final_node_id)
106            }
107            state => Err(MixnetContractError::UnexpectedNonRewardingEpochState {
108                current_state: *state,
109            }),
110        }
111    }
112
113    pub fn last_rewarded(&self) -> Result<NodeId, MixnetContractError> {
114        match self.state {
115            EpochState::Rewarding { last_rewarded, .. } => Ok(last_rewarded),
116            state => Err(MixnetContractError::UnexpectedNonRewardingEpochState {
117                current_state: state,
118            }),
119        }
120    }
121
122    pub fn ensure_is_in_event_reconciliation_state(&self) -> Result<(), MixnetContractError> {
123        if !matches!(self.state, EpochState::ReconcilingEvents) {
124            return Err(MixnetContractError::EpochNotInEventReconciliationState {
125                current_state: self.state,
126            });
127        }
128        Ok(())
129    }
130
131    pub fn ensure_is_in_expected_role_assignment_state(
132        &self,
133        caller: Role,
134    ) -> Result<(), MixnetContractError> {
135        let EpochState::RoleAssignment { next } = self.state else {
136            return Err(MixnetContractError::EpochNotInRoleAssignmentState {
137                current_state: self.state,
138            });
139        };
140
141        if caller != next {
142            return Err(MixnetContractError::UnexpectedRoleAssignment {
143                expected: next,
144                got: caller,
145            });
146        }
147
148        Ok(())
149    }
150
151    pub fn is_in_progress(&self) -> bool {
152        matches!(self.state, EpochState::InProgress)
153    }
154
155    pub fn is_rewarding(&self) -> bool {
156        matches!(self.state, EpochState::Rewarding { .. })
157    }
158
159    pub fn is_reconciling(&self) -> bool {
160        matches!(self.state, EpochState::ReconcilingEvents)
161    }
162}
163
164/// The state of the current rewarding epoch.
165#[cw_serde]
166#[derive(Copy)]
167pub enum EpochState {
168    /// Represents the state of an epoch that's in progress (well, duh.)
169    /// All actions are allowed to be issued.
170    #[serde(alias = "InProgress")]
171    InProgress,
172
173    /// Represents the state of an epoch when the rewarding entity has been decided on,
174    /// and the mixnodes are in the process of being rewarded for their work in this epoch.
175    #[serde(alias = "Rewarding")]
176    Rewarding {
177        /// The id of the last node that has already received its rewards.
178        last_rewarded: NodeId,
179
180        /// The id of the last node that's going to be rewarded before progressing into the next state.
181        final_node_id: NodeId,
182        // total_rewarded: u32,
183    },
184
185    /// Represents the state of an epoch when all mixnodes have already been rewarded for their work in this epoch
186    /// and all issued actions should now get resolved before being allowed to advance into the next epoch.
187    #[serde(alias = "ReconcilingEvents")]
188    ReconcilingEvents,
189
190    /// Represents the state of an epoch when all nodes have already been rewarded for their work in this epoch,
191    /// all issued actions got resolved and node roles should now be assigned before advancing into the next epoch.
192    RoleAssignment { next: Role },
193}
194
195impl Display for EpochState {
196    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
197        match self {
198            EpochState::InProgress => write!(f, "in progress"),
199            EpochState::Rewarding {
200                last_rewarded,
201                final_node_id,
202            } => write!(
203                f,
204                "mix rewarding (last rewarded: {last_rewarded}, final node: {final_node_id})"
205            ),
206            EpochState::ReconcilingEvents => write!(f, "event reconciliation"),
207            EpochState::RoleAssignment { next } => {
208                write!(f, "role assignment with next assignment for: {next}")
209            }
210        }
211    }
212}
213
214/// Specification of a rewarding interval.
215#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
216#[cfg_attr(
217    feature = "generate-ts",
218    ts(export, export_to = "ts-packages/types/src/types/rust/Interval.ts")
219)]
220#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
221#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
222pub struct Interval {
223    /// Monotonously increasing id of this interval.
224    #[cfg_attr(feature = "utoipa", schema(value_type = u32))]
225    id: IntervalId,
226
227    /// Number of epochs in this interval.
228    epochs_in_interval: u32,
229
230    // TODO add a better TS type generation
231    #[cfg_attr(feature = "generate-ts", ts(type = "string"))]
232    #[serde(with = "string_rfc3339_offset_date_time")]
233    // note: the `ts-rs failed to parse this attribute. It will be ignored.` warning emitted during
234    // compilation is fine (I guess). `ts-rs` can't handle `with` serde attribute, but that's okay
235    // since we explicitly specified this field should correspond to typescript's string
236    /// The timestamp indicating the start of the current rewarding epoch.
237    current_epoch_start: OffsetDateTime,
238
239    /// Monotonously increasing id of the current epoch in this interval.
240    #[cfg_attr(feature = "utoipa", schema(value_type = u32))]
241    current_epoch_id: EpochId,
242
243    /// The duration of all epochs in this interval.
244    #[cfg_attr(feature = "generate-ts", ts(type = "{ secs: number; nanos: number; }"))]
245    epoch_length: Duration,
246
247    /// The total amount of elapsed epochs since the first epoch of the first interval.
248    #[cfg_attr(feature = "utoipa", schema(value_type = u32))]
249    total_elapsed_epochs: EpochId,
250}
251
252impl JsonSchema for Interval {
253    fn schema_name() -> String {
254        "Interval".to_owned()
255    }
256
257    fn json_schema(r#gen: &mut SchemaGenerator) -> Schema {
258        let mut schema_object = SchemaObject {
259            instance_type: Some(InstanceType::Object.into()),
260            ..SchemaObject::default()
261        };
262
263        let object_validation = schema_object.object();
264        object_validation
265            .properties
266            .insert("id".to_owned(), r#gen.subschema_for::<IntervalId>());
267        object_validation.required.insert("id".to_owned());
268
269        object_validation.properties.insert(
270            "epochs_in_interval".to_owned(),
271            r#gen.subschema_for::<u32>(),
272        );
273        object_validation
274            .required
275            .insert("epochs_in_interval".to_owned());
276
277        // PrimitiveDateTime does not implement JsonSchema. However it has a custom
278        // serialization to string, so we just specify the schema to be String.
279        object_validation.properties.insert(
280            "current_epoch_start".to_owned(),
281            r#gen.subschema_for::<String>(),
282        );
283        object_validation
284            .required
285            .insert("current_epoch_start".to_owned());
286
287        object_validation.properties.insert(
288            "current_epoch_id".to_owned(),
289            r#gen.subschema_for::<EpochId>(),
290        );
291        object_validation
292            .required
293            .insert("current_epoch_id".to_owned());
294
295        object_validation
296            .properties
297            .insert("epoch_length".to_owned(), r#gen.subschema_for::<Duration>());
298        object_validation.required.insert("epoch_length".to_owned());
299
300        object_validation.properties.insert(
301            "total_elapsed_epochs".to_owned(),
302            r#gen.subschema_for::<EpochId>(),
303        );
304        object_validation
305            .required
306            .insert("total_elapsed_epochs".to_owned());
307
308        Schema::Object(schema_object)
309    }
310}
311
312impl Interval {
313    /// Initialize epoch in the contract with default values.
314    pub fn init_interval(epochs_in_interval: u32, epoch_length: Duration, env: &Env) -> Self {
315        // if this fails it means the value provided from the chain itself (via cosmwasm) is invalid,
316        // so we really have to panic here as anything beyond that point would be invalid anyway
317        #[allow(clippy::expect_used)]
318        let current_epoch_start =
319            OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64)
320                .expect("The timestamp provided via env.block.time is invalid");
321
322        Interval {
323            id: 0,
324            epochs_in_interval,
325            current_epoch_start,
326            current_epoch_id: 0,
327            epoch_length,
328            total_elapsed_epochs: 0,
329        }
330    }
331
332    pub const fn current_epoch_id(&self) -> EpochId {
333        self.current_epoch_id
334    }
335
336    pub const fn current_interval_id(&self) -> IntervalId {
337        self.id
338    }
339
340    pub const fn epochs_in_interval(&self) -> u32 {
341        self.epochs_in_interval
342    }
343
344    pub fn force_change_epochs_in_interval(&mut self, epochs_in_interval: u32) {
345        self.epochs_in_interval = epochs_in_interval;
346        if self.current_epoch_id >= epochs_in_interval {
347            // we have to go to the next interval as we can't
348            // have the same (interval, epoch) combo as we had in the past
349            self.id += self.current_epoch_id / epochs_in_interval;
350            self.current_epoch_id %= epochs_in_interval;
351        }
352    }
353
354    pub fn change_epoch_length(&mut self, epoch_length: Duration) {
355        self.epoch_length = epoch_length
356    }
357
358    pub const fn total_elapsed_epochs(&self) -> u32 {
359        self.total_elapsed_epochs
360    }
361
362    pub const fn current_epoch_absolute_id(&self) -> EpochId {
363        // since we count epochs starting from 0, if n epochs have elapsed, the current one has absolute id of n
364        self.total_elapsed_epochs
365    }
366
367    #[inline]
368    pub fn is_current_epoch_over(&self, env: &Env) -> bool {
369        self.current_epoch_end_unix_timestamp() <= env.block.time.seconds() as i64
370    }
371
372    pub fn ensure_current_epoch_is_over(&self, env: &Env) -> Result<(), MixnetContractError> {
373        if !self.is_current_epoch_over(env) {
374            return Err(MixnetContractError::EpochInProgress {
375                current_block_time: env.block.time.seconds(),
376                epoch_start: self.current_epoch_start_unix_timestamp(),
377                epoch_end: self.current_epoch_end_unix_timestamp(),
378            });
379        }
380        Ok(())
381    }
382
383    pub fn secs_until_current_epoch_end(&self, env: &Env) -> i64 {
384        if self.is_current_epoch_over(env) {
385            0
386        } else {
387            self.current_epoch_end_unix_timestamp() - env.block.time.seconds() as i64
388        }
389    }
390
391    #[inline]
392    pub fn is_current_interval_over(&self, env: &Env) -> bool {
393        self.current_interval_end_unix_timestamp() <= env.block.time.seconds() as i64
394    }
395
396    pub fn secs_until_current_interval_end(&self, env: &Env) -> i64 {
397        if self.is_current_interval_over(env) {
398            0
399        } else {
400            self.current_interval_end_unix_timestamp() - env.block.time.seconds() as i64
401        }
402    }
403
404    pub fn current_epoch_in_progress(&self, env: &Env) -> bool {
405        let block_time = env.block.time.seconds() as i64;
406        self.current_epoch_start_unix_timestamp() <= block_time
407            && block_time < self.current_epoch_end_unix_timestamp()
408    }
409
410    pub fn update_epoch_duration(&mut self, secs: u64) {
411        self.epoch_length = Duration::from_secs(secs);
412    }
413
414    pub const fn epoch_length_secs(&self) -> u64 {
415        self.epoch_length.as_secs()
416    }
417
418    /// Returns the next epoch. If if would result in advancing the interval,
419    /// the relevant changes are applied.
420    #[must_use]
421    pub fn advance_epoch(&self) -> Self {
422        // remember we start from 0th epoch, so if we're supposed to have 100 epochs in interval,
423        // epoch 99 is going to be the last one
424        if self.current_epoch_id == self.epochs_in_interval - 1 {
425            Interval {
426                id: self.id + 1,
427                epochs_in_interval: self.epochs_in_interval,
428                current_epoch_start: self.current_epoch_end(),
429                current_epoch_id: 0,
430                epoch_length: self.epoch_length,
431                total_elapsed_epochs: self.total_elapsed_epochs + 1,
432            }
433        } else {
434            Interval {
435                id: self.id,
436                epochs_in_interval: self.epochs_in_interval,
437                current_epoch_start: self.current_epoch_end(),
438                current_epoch_id: self.current_epoch_id + 1,
439                epoch_length: self.epoch_length,
440                total_elapsed_epochs: self.total_elapsed_epochs + 1,
441            }
442        }
443    }
444
445    /// Returns the starting datetime of this interval.
446    pub const fn current_epoch_start(&self) -> OffsetDateTime {
447        self.current_epoch_start
448    }
449
450    /// Returns the length of this interval.
451    pub const fn epoch_length(&self) -> Duration {
452        self.epoch_length
453    }
454
455    /// Returns the ending datetime of the current epoch.
456    pub fn current_epoch_end(&self) -> OffsetDateTime {
457        self.current_epoch_start + self.epoch_length
458    }
459
460    pub fn epochs_until_interval_end(&self) -> u32 {
461        self.epochs_in_interval - self.current_epoch_id
462    }
463
464    /// Returns the ending datetime of the current interval.
465    pub fn current_interval_end(&self) -> OffsetDateTime {
466        self.current_epoch_start + self.epochs_until_interval_end() * self.epoch_length
467    }
468
469    /// Returns the unix timestamp of the start of the current epoch.
470    pub const fn current_epoch_start_unix_timestamp(&self) -> i64 {
471        self.current_epoch_start().unix_timestamp()
472    }
473
474    /// Returns the unix timestamp of the end of the current epoch.
475    #[inline]
476    pub fn current_epoch_end_unix_timestamp(&self) -> i64 {
477        self.current_epoch_end().unix_timestamp()
478    }
479
480    /// Returns the unix timestamp of the end of the current interval.
481    #[inline]
482    pub fn current_interval_end_unix_timestamp(&self) -> i64 {
483        self.current_interval_end().unix_timestamp()
484    }
485}
486
487impl Display for Interval {
488    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
489        let length = self.epoch_length_secs();
490        let full_hours = length / 3600;
491        let rem = length % 3600;
492        write!(
493            f,
494            "Interval {}: epoch {}/{} (current epoch begun at: {}; epoch lengths: {}h {}s)",
495            self.id,
496            self.current_epoch_id + 1,
497            self.epochs_in_interval,
498            self.current_epoch_start,
499            full_hours,
500            rem
501        )
502    }
503}
504
505/// Information about the current rewarding interval.
506#[cw_serde]
507pub struct CurrentIntervalResponse {
508    /// Detailed information about the underlying interval.
509    pub interval: Interval,
510
511    /// The current blocktime
512    pub current_blocktime: u64,
513
514    /// Flag indicating whether the current interval is over and it should be advanced.
515    pub is_current_interval_over: bool,
516
517    /// Flag indicating whether the current epoch is over and it should be advanced.
518    pub is_current_epoch_over: bool,
519}
520
521impl CurrentIntervalResponse {
522    pub fn new(interval: Interval, env: Env) -> Self {
523        CurrentIntervalResponse {
524            interval,
525            current_blocktime: env.block.time.seconds(),
526            is_current_interval_over: interval.is_current_interval_over(&env),
527            is_current_epoch_over: interval.is_current_epoch_over(&env),
528        }
529    }
530
531    pub fn time_until_current_epoch_end(&self) -> Duration {
532        if self.is_current_epoch_over {
533            Duration::from_secs(0)
534        } else {
535            let remaining_secs =
536                self.interval.current_epoch_end_unix_timestamp() - self.current_blocktime as i64;
537            // this should never be negative, but better safe than sorry and guard ourselves against that case
538            if remaining_secs <= 0 {
539                Duration::from_secs(0)
540            } else {
541                Duration::from_secs(remaining_secs as u64)
542            }
543        }
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550    use cosmwasm_std::testing::mock_env;
551    use rand_chacha::rand_core::{RngCore, SeedableRng};
552
553    #[test]
554    fn advancing_epoch() {
555        // just advancing epoch
556        let interval = Interval {
557            id: 0,
558            epochs_in_interval: 100,
559            current_epoch_start: time::macros::datetime!(2021-08-23 12:00 UTC),
560            current_epoch_id: 23,
561            epoch_length: Duration::from_secs(60 * 60),
562            total_elapsed_epochs: 0,
563        };
564        let expected = Interval {
565            id: 0,
566            epochs_in_interval: 100,
567            current_epoch_start: time::macros::datetime!(2021-08-23 13:00 UTC),
568            current_epoch_id: 24,
569            epoch_length: Duration::from_secs(60 * 60),
570            total_elapsed_epochs: 1,
571        };
572        assert_eq!(expected, interval.advance_epoch());
573
574        // results in advancing interval
575        let interval = Interval {
576            id: 0,
577            epochs_in_interval: 100,
578            current_epoch_start: time::macros::datetime!(2021-08-23 12:00 UTC),
579            current_epoch_id: 99,
580            epoch_length: Duration::from_secs(60 * 60),
581            total_elapsed_epochs: 42,
582        };
583        let expected = Interval {
584            id: 1,
585            epochs_in_interval: 100,
586            current_epoch_start: time::macros::datetime!(2021-08-23 13:00 UTC),
587            current_epoch_id: 0,
588            epoch_length: Duration::from_secs(60 * 60),
589            total_elapsed_epochs: 43,
590        };
591
592        assert_eq!(expected, interval.advance_epoch())
593    }
594
595    #[test]
596    fn checking_for_epoch_ends() {
597        let env = mock_env();
598
599        // epoch just begun
600        let mut interval = Interval {
601            id: 0,
602            epochs_in_interval: 100,
603            current_epoch_start: OffsetDateTime::from_unix_timestamp(
604                env.block.time.seconds() as i64 - 100,
605            )
606            .unwrap(),
607            current_epoch_id: 23,
608            epoch_length: Duration::from_secs(60 * 60),
609            total_elapsed_epochs: 0,
610        };
611        assert!(!interval.is_current_epoch_over(&env));
612
613        // current time == current epoch start
614        interval.current_epoch_start =
615            OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64).unwrap();
616        assert!(!interval.is_current_epoch_over(&env));
617
618        // epoch HASN'T yet begun (weird edge case, but can happen if we decide to manually adjust things)
619        interval.current_epoch_start =
620            OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64 + 100).unwrap();
621        assert!(!interval.is_current_epoch_over(&env));
622
623        // current_time = EXACTLY end of the epoch
624        interval.current_epoch_start =
625            OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64).unwrap()
626                - interval.epoch_length;
627        assert!(interval.is_current_epoch_over(&env));
628
629        // revert time a bit more
630        interval.current_epoch_start -= Duration::from_secs(42);
631        assert!(interval.is_current_epoch_over(&env));
632
633        // revert by A LOT -> epoch still should be in finished state
634        interval.current_epoch_start -= Duration::from_secs(5 * 31 * 60 * 60);
635        assert!(interval.is_current_epoch_over(&env));
636    }
637
638    #[test]
639    fn interval_end() {
640        let mut interval = Interval {
641            id: 0,
642            epochs_in_interval: 100,
643            current_epoch_start: time::macros::datetime!(2021-08-23 12:00 UTC),
644            current_epoch_id: 99,
645            epoch_length: Duration::from_secs(60 * 60),
646            total_elapsed_epochs: 0,
647        };
648
649        assert_eq!(
650            interval.current_epoch_start + interval.epoch_length,
651            interval.current_interval_end()
652        );
653
654        interval.current_epoch_id -= 1;
655        assert_eq!(
656            interval.current_epoch_start + 2 * interval.epoch_length,
657            interval.current_interval_end()
658        );
659
660        interval.current_epoch_id -= 10;
661        assert_eq!(
662            interval.current_epoch_start + 12 * interval.epoch_length,
663            interval.current_interval_end()
664        );
665
666        interval.current_epoch_id = 0;
667        assert_eq!(
668            interval.current_epoch_start + interval.epochs_in_interval * interval.epoch_length,
669            interval.current_interval_end()
670        );
671    }
672
673    #[test]
674    fn checking_for_interval_ends() {
675        let env = mock_env();
676
677        let epoch_length = Duration::from_secs(60 * 60);
678
679        let mut interval = Interval {
680            id: 0,
681            epochs_in_interval: 100,
682            current_epoch_start: OffsetDateTime::from_unix_timestamp(
683                env.block.time.seconds() as i64
684            )
685            .unwrap(),
686            current_epoch_id: 98,
687            epoch_length,
688            total_elapsed_epochs: 0,
689        };
690
691        // current epoch just started (we still have to finish 2 epochs)
692        assert!(!interval.is_current_interval_over(&env));
693
694        // still need to finish the 99th epoch
695        interval.current_epoch_start -= epoch_length;
696        assert!(!interval.is_current_interval_over(&env));
697
698        // it JUST finished
699        interval.current_epoch_start -= epoch_length;
700        assert!(interval.is_current_interval_over(&env));
701
702        // nobody updated the interval data, but the current one should still be in finished state
703        interval.current_epoch_start -= 10 * epoch_length;
704        assert!(interval.is_current_interval_over(&env));
705    }
706
707    #[test]
708    fn getting_current_full_epoch_id() {
709        let env = mock_env();
710        let dummy_seed = [42u8; 32];
711        let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
712
713        let epoch_length = Duration::from_secs(60 * 60);
714
715        let mut interval = Interval::init_interval(100, epoch_length, &env);
716
717        // normal situation
718        for i in 0u32..2000 {
719            assert_eq!(interval.current_epoch_absolute_id(), i);
720            interval = interval.advance_epoch();
721        }
722
723        let mut interval = Interval::init_interval(100, epoch_length, &env);
724
725        for i in 0u32..2000 {
726            // every few epochs decide to change epochs in interval
727            if i % 7 == 0 {
728                let new_epochs_in_interval = (rng.next_u32() % 200) + 42;
729                interval.force_change_epochs_in_interval(new_epochs_in_interval)
730            }
731
732            // make sure full epoch id is always monotonically increasing
733            assert_eq!(interval.current_epoch_absolute_id(), i);
734
735            interval = interval.advance_epoch();
736        }
737    }
738}