1use 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 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 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 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}