nym_api_requests/models/
mixnet.rs

1// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use nym_mixnet_contract_common::nym_node::Role;
5use nym_mixnet_contract_common::{EpochId, KeyRotationId, KeyRotationState, NodeId};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::cmp::Ordering;
9use std::time::Duration;
10use time::OffsetDateTime;
11use tracing::warn;
12use utoipa::ToSchema;
13
14#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
15pub struct KeyRotationInfoResponse {
16    #[serde(flatten)]
17    pub details: KeyRotationDetails,
18
19    // helper field that holds calculated data based on the `details` field
20    // this is to expose the information in a format more easily accessible by humans
21    // without having to do any calculations
22    pub progress: KeyRotationProgressInfo,
23}
24
25impl From<KeyRotationDetails> for KeyRotationInfoResponse {
26    fn from(details: KeyRotationDetails) -> Self {
27        KeyRotationInfoResponse {
28            details,
29            progress: KeyRotationProgressInfo {
30                current_key_rotation_id: details.current_key_rotation_id(),
31                current_rotation_starting_epoch: details.current_rotation_starting_epoch_id(),
32                current_rotation_ending_epoch: details.current_rotation_starting_epoch_id()
33                    + details.key_rotation_state.validity_epochs
34                    - 1,
35            },
36        }
37    }
38}
39
40#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
41pub struct KeyRotationProgressInfo {
42    pub current_key_rotation_id: u32,
43
44    pub current_rotation_starting_epoch: u32,
45
46    pub current_rotation_ending_epoch: u32,
47}
48
49#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
50pub struct KeyRotationDetails {
51    pub key_rotation_state: KeyRotationState,
52
53    #[schema(value_type = u32)]
54    pub current_absolute_epoch_id: EpochId,
55
56    #[serde(with = "time::serde::rfc3339")]
57    #[schemars(with = "String")]
58    #[schema(value_type = String)]
59    pub current_epoch_start: OffsetDateTime,
60
61    pub epoch_duration: Duration,
62}
63
64impl KeyRotationDetails {
65    pub fn current_key_rotation_id(&self) -> u32 {
66        self.key_rotation_state
67            .key_rotation_id(self.current_absolute_epoch_id)
68    }
69
70    pub fn next_rotation_starting_epoch_id(&self) -> EpochId {
71        self.key_rotation_state
72            .next_rotation_starting_epoch_id(self.current_absolute_epoch_id)
73    }
74
75    pub fn current_rotation_starting_epoch_id(&self) -> EpochId {
76        self.key_rotation_state
77            .current_rotation_starting_epoch_id(self.current_absolute_epoch_id)
78    }
79
80    fn current_epoch_progress(&self, now: OffsetDateTime) -> f32 {
81        let elapsed = (now - self.current_epoch_start).as_seconds_f32();
82        elapsed / self.epoch_duration.as_secs_f32()
83    }
84
85    pub fn is_epoch_stuck(&self) -> bool {
86        let now = OffsetDateTime::now_utc();
87        let progress = self.current_epoch_progress(now);
88        if progress > 1. {
89            let into_next = 1. - progress;
90            // if epoch hasn't progressed for more than 20% of its duration, mark is as stuck
91            if into_next > 0.2 {
92                let diff_time =
93                    Duration::from_secs_f32(into_next * self.epoch_duration.as_secs_f32());
94                let expected_epoch_end = self.current_epoch_start + self.epoch_duration;
95                warn!("the current epoch is expected to have been over by {expected_epoch_end}. it's already {} overdue!", humantime_serde::re::humantime::format_duration(diff_time));
96                return true;
97            }
98        }
99
100        false
101    }
102
103    // based on the current **TIME**, determine what's the expected current rotation id
104    pub fn expected_current_rotation_id(&self) -> KeyRotationId {
105        let now = OffsetDateTime::now_utc();
106        let current_end = now + self.epoch_duration;
107        if now < current_end {
108            return self
109                .key_rotation_state
110                .key_rotation_id(self.current_absolute_epoch_id);
111        }
112
113        let diff = now - current_end;
114        let passed_epochs = diff / self.epoch_duration;
115        let expected_current_epoch = self.current_absolute_epoch_id + passed_epochs.floor() as u32;
116
117        self.key_rotation_state
118            .key_rotation_id(expected_current_epoch)
119    }
120
121    pub fn until_next_rotation(&self) -> Option<Duration> {
122        let current_epoch_progress = self.current_epoch_progress(OffsetDateTime::now_utc());
123        if current_epoch_progress > 1. {
124            return None;
125        }
126
127        let next_rotation_epoch = self.next_rotation_starting_epoch_id();
128        let full_remaining =
129            (next_rotation_epoch - self.current_absolute_epoch_id).checked_add(1)?;
130
131        let epochs_until_next_rotation = (1. - current_epoch_progress) + full_remaining as f32;
132
133        Some(Duration::from_secs_f32(
134            epochs_until_next_rotation * self.epoch_duration.as_secs_f32(),
135        ))
136    }
137
138    pub fn epoch_start_time(&self, absolute_epoch_id: EpochId) -> OffsetDateTime {
139        match absolute_epoch_id.cmp(&self.current_absolute_epoch_id) {
140            Ordering::Less => {
141                let diff = self.current_absolute_epoch_id - absolute_epoch_id;
142                self.current_epoch_start - diff * self.epoch_duration
143            }
144            Ordering::Equal => self.current_epoch_start,
145            Ordering::Greater => {
146                let diff = absolute_epoch_id - self.current_absolute_epoch_id;
147                self.current_epoch_start + diff * self.epoch_duration
148            }
149        }
150    }
151}
152
153#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
154pub struct RewardedSetResponse {
155    #[serde(default)]
156    #[schema(value_type = u32)]
157    pub epoch_id: EpochId,
158
159    pub entry_gateways: Vec<NodeId>,
160
161    pub exit_gateways: Vec<NodeId>,
162
163    pub layer1: Vec<NodeId>,
164
165    pub layer2: Vec<NodeId>,
166
167    pub layer3: Vec<NodeId>,
168
169    pub standby: Vec<NodeId>,
170}
171
172impl From<RewardedSetResponse> for nym_mixnet_contract_common::EpochRewardedSet {
173    fn from(res: RewardedSetResponse) -> Self {
174        nym_mixnet_contract_common::EpochRewardedSet {
175            epoch_id: res.epoch_id,
176            assignment: nym_mixnet_contract_common::RewardedSet {
177                entry_gateways: res.entry_gateways,
178                exit_gateways: res.exit_gateways,
179                layer1: res.layer1,
180                layer2: res.layer2,
181                layer3: res.layer3,
182                standby: res.standby,
183            },
184        }
185    }
186}
187
188impl From<nym_mixnet_contract_common::EpochRewardedSet> for RewardedSetResponse {
189    fn from(r: nym_mixnet_contract_common::EpochRewardedSet) -> Self {
190        RewardedSetResponse {
191            epoch_id: r.epoch_id,
192            entry_gateways: r.assignment.entry_gateways,
193            exit_gateways: r.assignment.exit_gateways,
194            layer1: r.assignment.layer1,
195            layer2: r.assignment.layer2,
196            layer3: r.assignment.layer3,
197            standby: r.assignment.standby,
198        }
199    }
200}
201
202#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, JsonSchema, ToSchema)]
203#[serde(rename_all = "camelCase")]
204#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
205#[cfg_attr(
206    feature = "generate-ts",
207    ts(export, export_to = "ts-packages/types/src/types/rust/DisplayRole.ts")
208)]
209pub enum DisplayRole {
210    EntryGateway,
211    Layer1,
212    Layer2,
213    Layer3,
214    ExitGateway,
215    Standby,
216}
217
218impl From<Role> for DisplayRole {
219    fn from(role: Role) -> Self {
220        match role {
221            Role::EntryGateway => DisplayRole::EntryGateway,
222            Role::Layer1 => DisplayRole::Layer1,
223            Role::Layer2 => DisplayRole::Layer2,
224            Role::Layer3 => DisplayRole::Layer3,
225            Role::ExitGateway => DisplayRole::ExitGateway,
226            Role::Standby => DisplayRole::Standby,
227        }
228    }
229}
230
231impl From<DisplayRole> for Role {
232    fn from(role: DisplayRole) -> Self {
233        match role {
234            DisplayRole::EntryGateway => Role::EntryGateway,
235            DisplayRole::Layer1 => Role::Layer1,
236            DisplayRole::Layer2 => Role::Layer2,
237            DisplayRole::Layer3 => Role::Layer3,
238            DisplayRole::ExitGateway => Role::ExitGateway,
239            DisplayRole::Standby => Role::Standby,
240        }
241    }
242}