1use crate::ecash::models::EcashSignerStatusResponse;
5use crate::models::tendermint_types::{BlockHeader, BlockId};
6use crate::models::{ChainStatus, SignerInformationResponse};
7use crate::signable::SignedMessage;
8use nym_coconut_dkg_common::types::EpochId;
9use nym_crypto::asymmetric::ed25519::PublicKey;
10use nym_ecash_signer_check_types::helper_traits::{
11 ChainResponse, LegacyChainResponse, LegacySignerResponse, SignerResponse, TimestampedResponse,
12 Verifiable,
13};
14use nym_ecash_signer_check_types::status::SignerResult;
15use schemars::JsonSchema;
16use serde::{Deserialize, Serialize};
17use std::time::Duration;
18use time::OffsetDateTime;
19use utoipa::ToSchema;
20
21pub type ChainBlocksStatusResponse = SignedMessage<ChainBlocksStatusResponseBody>;
22pub type SignersStatusResponse = SignedMessage<SignersStatusResponseBody>;
23pub type DetailedSignersStatusResponse = SignedMessage<DetailedSignersStatusResponseBody>;
24
25#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
26#[serde(rename_all = "camelCase")]
27pub struct SignersStatusResponseBody {
28 #[serde(with = "time::serde::rfc3339")]
29 #[schema(value_type = String)]
30 pub as_at: OffsetDateTime,
31
32 pub overview: SignersStatusOverview,
33
34 pub results: Vec<MinimalSignerResult>,
35}
36
37pub type TypedSignerResult = SignerResult<
38 SignerInformationResponse,
39 EcashSignerStatusResponse,
40 ChainStatusResponse,
41 ChainBlocksStatusResponse,
42>;
43
44#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
45#[serde(rename_all = "camelCase")]
46pub struct MinimalSignerResult {
47 pub announce_address: String,
48 pub owner_address: String,
49 pub node_index: u64,
50 pub public_key: String,
51
52 pub local_chain_working: bool,
53 pub credential_issuance_available: bool,
54}
55
56impl From<&TypedSignerResult> for MinimalSignerResult {
57 fn from(result: &TypedSignerResult) -> MinimalSignerResult {
58 MinimalSignerResult {
59 announce_address: result.information.announce_address.clone(),
60 owner_address: result.information.owner_address.clone(),
61 node_index: result.information.node_index,
62 public_key: result.information.public_key.clone(),
63 local_chain_working: result.chain_available(),
64 credential_issuance_available: result.signing_available(),
65 }
66 }
67}
68
69#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
70#[serde(rename_all = "camelCase")]
71pub struct DetailedSignersStatusResponseBody {
72 #[serde(with = "time::serde::rfc3339")]
73 #[schema(value_type = String)]
74 pub as_at: OffsetDateTime,
75
76 pub overview: SignersStatusOverview,
77
78 pub details: Vec<TypedSignerResult>,
79}
80
81#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
82#[serde(rename_all = "camelCase")]
83pub struct SignersStatusOverview {
84 #[schema(value_type = Option<u64>)]
85 pub epoch_id: Option<EpochId>,
86
87 pub signing_threshold: Option<u64>,
88 pub threshold_available: Option<bool>,
89
90 pub total_signers: usize,
91 pub unreachable_signers: usize,
92 pub malformed_signers: usize,
93
94 pub unknown_local_chain_status: usize,
96 pub working_local_chain: usize,
97
98 pub provably_stalled_local_chain: usize,
100 pub unprovably_stalled_local_chain: usize,
101
102 pub unknown_credential_issuance_status: usize,
104 pub working_credential_issuance: usize,
105
106 pub provably_unavailable_credential_issuance: usize,
108 pub unprovably_unavailable_credential_issuance: usize,
109}
110
111impl SignersStatusOverview {
112 pub fn new(results: &[TypedSignerResult], signing_threshold: Option<u64>) -> Self {
113 let epoch_id = results.first().map(|r| r.dkg_epoch_id);
114
115 let mut unreachable_signers = 0;
116 let mut malformed_signers = 0;
117 let mut unknown_local_chain_status = 0;
118 let mut working_local_chain = 0;
119 let mut provably_stalled_local_chain = 0;
120 let mut unprovably_stalled_local_chain = 0;
121 let mut unknown_credential_issuance_status = 0;
122 let mut working_credential_issuance = 0;
123 let mut provably_unavailable_credential_issuance = 0;
124 let mut unprovably_unavailable_credential_issuance = 0;
125
126 for result in results {
127 if result.signer_unreachable() {
128 unreachable_signers += 1;
129 }
130 if result.malformed_details() {
131 malformed_signers += 1;
132 }
133
134 if result.unknown_chain_status() {
135 unknown_local_chain_status += 1;
136 }
137 if result.chain_available() {
138 working_local_chain += 1;
139 }
140 if result.chain_provably_stalled() {
141 provably_stalled_local_chain += 1;
142 }
143 if result.chain_unprovably_stalled() {
144 unprovably_stalled_local_chain += 1;
145 }
146
147 if result.unknown_signing_status() {
148 unknown_credential_issuance_status += 1;
149 }
150 if result.signing_available() {
151 working_credential_issuance += 1;
152 }
153 if result.signing_provably_unavailable() {
154 provably_unavailable_credential_issuance += 1;
155 }
156 if result.signing_unprovably_unavailable() {
157 unprovably_unavailable_credential_issuance += 1;
158 }
159 }
160
161 SignersStatusOverview {
162 epoch_id,
163 signing_threshold,
164 threshold_available: signing_threshold.map(|threshold| {
165 (working_local_chain as u64) >= threshold
166 && (working_credential_issuance as u64) >= threshold
167 }),
168 total_signers: results.len(),
169 unreachable_signers,
170 malformed_signers,
171 unknown_local_chain_status,
172 working_local_chain,
173 provably_stalled_local_chain,
174 unprovably_stalled_local_chain,
175 unknown_credential_issuance_status,
176 working_credential_issuance,
177 provably_unavailable_credential_issuance,
178 unprovably_unavailable_credential_issuance,
179 }
180 }
181}
182
183#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
184#[serde(rename_all = "camelCase")]
185pub struct ChainBlocksStatusResponseBody {
186 #[serde(with = "time::serde::rfc3339")]
187 #[schema(value_type = String)]
188 pub current_time: OffsetDateTime,
189
190 pub latest_cached_block: Option<DetailedChainStatus>,
191
192 pub chain_status: ChainStatus,
194}
195
196#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
197pub struct ChainStatusResponse {
198 pub connected_nyxd: String,
199 pub status: DetailedChainStatus,
200}
201
202#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
203pub struct DetailedChainStatus {
204 pub abci: crate::models::tendermint_types::AbciInfo,
205 pub latest_block: BlockInfo,
206}
207
208impl DetailedChainStatus {
209 pub fn stall_status(&self, now: OffsetDateTime, threshold: Duration) -> ChainStatus {
210 let block_time: OffsetDateTime = self.latest_block.block.header.time.into();
211 let diff = now - block_time;
212 if diff > threshold {
213 ChainStatus::Stalled {
214 approximate_amount: diff.unsigned_abs(),
215 }
216 } else {
217 ChainStatus::Synced
218 }
219 }
220}
221
222#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
223pub struct BlockInfo {
224 pub block_id: BlockId,
225 pub block: FullBlockInfo,
226 }
228
229impl From<tendermint_rpc::endpoint::block::Response> for BlockInfo {
230 fn from(value: tendermint_rpc::endpoint::block::Response) -> Self {
231 BlockInfo {
232 block_id: value.block_id.into(),
233 block: FullBlockInfo {
234 header: value.block.header.into(),
235 },
236 }
237 }
238}
239
240#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
241pub struct FullBlockInfo {
242 pub header: BlockHeader,
243}
244
245pub mod tendermint_types {
247 use schemars::JsonSchema;
248 use serde::{Deserialize, Serialize};
249 use tendermint::abci::response::Info;
250 use tendermint::block::header::Version;
251 use tendermint::{block, Hash};
252 use utoipa::ToSchema;
253
254 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
255 pub struct AbciInfo {
256 pub data: String,
258
259 pub version: String,
261
262 pub app_version: u64,
264
265 pub last_block_height: u64,
267
268 pub last_block_app_hash: String,
270 }
271
272 impl From<Info> for AbciInfo {
273 fn from(value: Info) -> Self {
274 AbciInfo {
275 data: value.data,
276 version: value.version,
277 app_version: value.app_version,
278 last_block_height: value.last_block_height.value(),
279 last_block_app_hash: value.last_block_app_hash.to_string(),
280 }
281 }
282 }
283
284 #[derive(
289 Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, ToSchema,
290 )]
291 pub struct HeaderVersion {
292 pub block: u64,
294
295 pub app: u64,
297 }
298
299 impl From<tendermint::block::header::Version> for HeaderVersion {
300 fn from(value: Version) -> Self {
301 HeaderVersion {
302 block: value.block,
303 app: value.app,
304 }
305 }
306 }
307
308 #[derive(
320 Serialize,
321 Deserialize,
322 Copy,
323 Clone,
324 Debug,
325 Default,
326 Hash,
327 Eq,
328 PartialEq,
329 PartialOrd,
330 Ord,
331 JsonSchema,
332 ToSchema,
333 )]
334 pub struct BlockId {
335 #[schemars(with = "String")]
338 #[schema(value_type = String)]
339 pub hash: Hash,
340
341 pub part_set_header: PartSetHeader,
358 }
359
360 impl From<block::Id> for BlockId {
361 fn from(value: block::Id) -> Self {
362 BlockId {
363 hash: value.hash,
364 part_set_header: value.part_set_header.into(),
365 }
366 }
367 }
368
369 #[derive(
371 Clone,
372 Copy,
373 Debug,
374 Default,
375 Hash,
376 Eq,
377 PartialEq,
378 PartialOrd,
379 Ord,
380 Deserialize,
381 Serialize,
382 JsonSchema,
383 ToSchema,
384 )]
385 #[non_exhaustive]
386 pub struct PartSetHeader {
387 pub total: u32,
389
390 #[schemars(with = "String")]
392 #[schema(value_type = String)]
393 pub hash: Hash,
394 }
395
396 impl From<tendermint::block::parts::Header> for PartSetHeader {
397 fn from(value: block::parts::Header) -> Self {
398 PartSetHeader {
399 total: value.total,
400 hash: value.hash,
401 }
402 }
403 }
404
405 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
411 pub struct BlockHeader {
412 pub version: HeaderVersion,
414
415 pub chain_id: String,
417
418 pub height: u64,
420
421 #[schemars(with = "String")]
423 #[schema(value_type = String)]
424 pub time: tendermint::Time,
425
426 pub last_block_id: Option<BlockId>,
428
429 #[schemars(with = "Option<String>")]
431 #[schema(value_type = Option<String>)]
432 pub last_commit_hash: Option<Hash>,
433
434 #[schemars(with = "Option<String>")]
436 #[schema(value_type = Option<String>)]
437 pub data_hash: Option<Hash>,
438
439 #[schemars(with = "String")]
441 #[schema(value_type = String)]
442 pub validators_hash: Hash,
443
444 #[schemars(with = "String")]
446 #[schema(value_type = String)]
447 pub next_validators_hash: Hash,
448
449 #[schemars(with = "String")]
451 #[schema(value_type = String)]
452 pub consensus_hash: Hash,
453
454 #[schemars(with = "String")]
456 #[schema(value_type = String)]
457 pub app_hash: Hash,
458
459 #[schemars(with = "Option<String>")]
461 #[schema(value_type = Option<String>)]
462 pub last_results_hash: Option<Hash>,
463
464 #[schemars(with = "Option<String>")]
466 #[schema(value_type = Option<String>)]
467 pub evidence_hash: Option<Hash>,
468
469 #[serde(with = "nym_serde_helpers::hex")]
471 #[schemars(with = "String")]
472 #[schema(value_type = String)]
473 pub proposer_address: Vec<u8>,
474 }
475
476 impl From<block::Header> for BlockHeader {
477 fn from(value: block::Header) -> Self {
478 BlockHeader {
479 version: value.version.into(),
480 chain_id: value.chain_id.to_string(),
481 height: value.height.value(),
482 time: value.time,
483 last_block_id: value.last_block_id.map(Into::into),
484 last_commit_hash: value.last_commit_hash,
485 data_hash: value.data_hash,
486 validators_hash: value.validators_hash,
487 next_validators_hash: value.next_validators_hash,
488 consensus_hash: value.consensus_hash,
489 app_hash: Hash::try_from(value.app_hash.as_bytes().to_vec()).unwrap_or_default(),
490 last_results_hash: value.last_results_hash,
491 evidence_hash: value.evidence_hash,
492 proposer_address: value.proposer_address.as_bytes().to_vec(),
493 }
494 }
495 }
496}
497
498impl LegacyChainResponse for ChainStatusResponse {
501 fn chain_synced(&self, now: OffsetDateTime, stall_threshold: Duration) -> bool {
502 self.status.stall_status(now, stall_threshold).is_synced()
503 }
504}
505
506impl Verifiable for ChainBlocksStatusResponse {
507 fn verify_signature(&self, pub_key: &PublicKey) -> bool {
508 self.verify_signature(pub_key)
509 }
510}
511
512impl TimestampedResponse for ChainBlocksStatusResponse {
513 fn timestamp(&self) -> OffsetDateTime {
514 self.body.current_time
515 }
516}
517
518impl ChainResponse for ChainBlocksStatusResponse {
519 fn chain_synced(&self) -> bool {
520 self.body.chain_status.is_synced()
521 }
522}
523
524impl LegacySignerResponse for SignerInformationResponse {
525 fn signer_identity(&self) -> &str {
526 &self.identity
527 }
528
529 fn signer_verification_key(&self) -> &Option<String> {
530 &self.verification_key
531 }
532}
533
534impl Verifiable for EcashSignerStatusResponse {
535 fn verify_signature(&self, pub_key: &PublicKey) -> bool {
536 self.verify_signature(pub_key)
537 }
538}
539
540impl TimestampedResponse for EcashSignerStatusResponse {
541 fn timestamp(&self) -> OffsetDateTime {
542 self.body.current_time
543 }
544}
545
546impl SignerResponse for EcashSignerStatusResponse {
547 fn has_signing_keys(&self) -> bool {
548 self.body.has_signing_keys
549 }
550
551 fn signer_disabled(&self) -> bool {
552 self.body.signer_disabled
553 }
554
555 fn is_ecash_signer(&self) -> bool {
556 self.body.is_ecash_signer
557 }
558
559 fn dkg_ecash_epoch_id(&self) -> EpochId {
560 self.body.dkg_ecash_epoch_id
561 }
562}