1use 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
20pub(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#[cw_serde]
70pub struct EpochStatus {
71 pub being_advanced_by: Addr,
75
76 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#[cw_serde]
166#[derive(Copy)]
167pub enum EpochState {
168 #[serde(alias = "InProgress")]
171 InProgress,
172
173 #[serde(alias = "Rewarding")]
176 Rewarding {
177 last_rewarded: NodeId,
179
180 final_node_id: NodeId,
182 },
184
185 #[serde(alias = "ReconcilingEvents")]
188 ReconcilingEvents,
189
190 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#[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 #[cfg_attr(feature = "utoipa", schema(value_type = u32))]
225 id: IntervalId,
226
227 epochs_in_interval: u32,
229
230 #[cfg_attr(feature = "generate-ts", ts(type = "string"))]
232 #[serde(with = "string_rfc3339_offset_date_time")]
233 current_epoch_start: OffsetDateTime,
238
239 #[cfg_attr(feature = "utoipa", schema(value_type = u32))]
241 current_epoch_id: EpochId,
242
243 #[cfg_attr(feature = "generate-ts", ts(type = "{ secs: number; nanos: number; }"))]
245 epoch_length: Duration,
246
247 #[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 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 pub fn init_interval(epochs_in_interval: u32, epoch_length: Duration, env: &Env) -> Self {
315 #[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 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 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 #[must_use]
421 pub fn advance_epoch(&self) -> Self {
422 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 pub const fn current_epoch_start(&self) -> OffsetDateTime {
447 self.current_epoch_start
448 }
449
450 pub const fn epoch_length(&self) -> Duration {
452 self.epoch_length
453 }
454
455 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 pub fn current_interval_end(&self) -> OffsetDateTime {
466 self.current_epoch_start + self.epochs_until_interval_end() * self.epoch_length
467 }
468
469 pub const fn current_epoch_start_unix_timestamp(&self) -> i64 {
471 self.current_epoch_start().unix_timestamp()
472 }
473
474 #[inline]
476 pub fn current_epoch_end_unix_timestamp(&self) -> i64 {
477 self.current_epoch_end().unix_timestamp()
478 }
479
480 #[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#[cw_serde]
507pub struct CurrentIntervalResponse {
508 pub interval: Interval,
510
511 pub current_blocktime: u64,
513
514 pub is_current_interval_over: bool,
516
517 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 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 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 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 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 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 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 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 interval.current_epoch_start -= Duration::from_secs(42);
631 assert!(interval.is_current_epoch_over(&env));
632
633 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 assert!(!interval.is_current_interval_over(&env));
693
694 interval.current_epoch_start -= epoch_length;
696 assert!(!interval.is_current_interval_over(&env));
697
698 interval.current_epoch_start -= epoch_length;
700 assert!(interval.is_current_interval_over(&env));
701
702 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 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 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 assert_eq!(interval.current_epoch_absolute_id(), i);
734
735 interval = interval.advance_epoch();
736 }
737 }
738}