1use 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
19pub(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 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 InProgress,
158
159 Rewarding {
162 last_rewarded: MixId,
163
164 final_node_id: MixId,
165 },
167
168 ReconcilingEvents,
171
172 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 #[cfg_attr(feature = "generate-ts", ts(type = "string"))]
206 #[serde(with = "string_rfc3339_offset_date_time")]
207 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 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 pub fn init_interval(epochs_in_interval: u32, epoch_length: Duration, env: &Env) -> Self {
279 #[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 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 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 #[must_use]
374 pub fn advance_epoch(&self) -> Self {
375 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 pub const fn current_epoch_start(&self) -> OffsetDateTime {
400 self.current_epoch_start
401 }
402
403 pub const fn epoch_length(&self) -> Duration {
405 self.epoch_length
406 }
407
408 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 pub fn current_interval_end(&self) -> OffsetDateTime {
419 self.current_epoch_start + self.epochs_until_interval_end() * self.epoch_length
420 }
421
422 pub const fn current_epoch_start_unix_timestamp(&self) -> i64 {
424 self.current_epoch_start().unix_timestamp()
425 }
426
427 #[inline]
429 pub fn current_epoch_end_unix_timestamp(&self) -> i64 {
430 self.current_epoch_end().unix_timestamp()
431 }
432
433 #[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 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 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 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 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 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 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 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 interval.current_epoch_start -= Duration::from_secs(42);
660 assert!(interval.is_current_epoch_over(&env));
661
662 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 assert!(!interval.is_current_interval_over(&env));
722
723 interval.current_epoch_start -= epoch_length;
725 assert!(!interval.is_current_interval_over(&env));
726
727 interval.current_epoch_start -= epoch_length;
729 assert!(interval.is_current_interval_over(&env));
730
731 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 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 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 assert_eq!(interval.current_epoch_absolute_id(), i);
763
764 interval = interval.advance_epoch();
765 }
766 }
767}