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