nym_api_requests/models/
network.rs

1// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use 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    // unreachable or outdated
95    pub unknown_local_chain_status: usize,
96    pub working_local_chain: usize,
97
98    // i.e. provided signature
99    pub provably_stalled_local_chain: usize,
100    pub unprovably_stalled_local_chain: usize,
101
102    // unreachable or outdated
103    pub unknown_credential_issuance_status: usize,
104    pub working_credential_issuance: usize,
105
106    // i.e. provided signature
107    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    // explicit indication of THIS signer whether it thinks the chain is stalled
193    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    // if necessary we might put block data here later too
227}
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
245// copy tendermint types definitions whilst deriving schema types on them and dropping unwanted fields
246pub 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        /// Some arbitrary information.
257        pub data: String,
258
259        /// The application software semantic version.
260        pub version: String,
261
262        /// The application protocol version.
263        pub app_version: u64,
264
265        /// The latest block for which the app has called [`Commit`].
266        pub last_block_height: u64,
267
268        /// The latest result of [`Commit`].
269        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    /// `Version` contains the protocol version for the blockchain and the
285    /// application.
286    ///
287    /// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#version>
288    #[derive(
289        Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, ToSchema,
290    )]
291    pub struct HeaderVersion {
292        /// Block version
293        pub block: u64,
294
295        /// App version
296        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    /// Block identifiers which contain two distinct Merkle roots of the block,
309    /// as well as the number of parts in the block.
310    ///
311    /// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#blockid>
312    ///
313    /// Default implementation is an empty Id as defined by the Go implementation in
314    /// <https://github.com/tendermint/tendermint/blob/1635d1339c73ae6a82e062cd2dc7191b029efa14/types/block.go#L1204>.
315    ///
316    /// If the Hash is empty in BlockId, the BlockId should be empty (encoded to None).
317    /// This is implemented outside of this struct. Use the Default trait to check for an empty BlockId.
318    /// See: <https://github.com/informalsystems/tendermint-rs/issues/663>
319    #[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        /// The block's main hash is the Merkle root of all the fields in the
336        /// block header.
337        #[schemars(with = "String")]
338        #[schema(value_type = String)]
339        pub hash: Hash,
340
341        /// Parts header (if available) is used for secure gossipping of the block
342        /// during consensus. It is the Merkle root of the complete serialized block
343        /// cut into parts.
344        ///
345        /// PartSet is used to split a byteslice of data into parts (pieces) for
346        /// transmission. By splitting data into smaller parts and computing a
347        /// Merkle root hash on the list, you can verify that a part is
348        /// legitimately part of the complete data, and the part can be forwarded
349        /// to other peers before all the parts are known. In short, it's a fast
350        /// way to propagate a large file over a gossip network.
351        ///
352        /// <https://github.com/tendermint/tendermint/wiki/Block-Structure#partset>
353        ///
354        /// PartSetHeader in protobuf is defined as never nil using the gogoproto
355        /// annotations. This does not translate to Rust, but we can indicate this
356        /// in the domain type.
357        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    /// Block parts header
370    #[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        /// Number of parts in this block
388        pub total: u32,
389
390        /// Hash of the parts set header,
391        #[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    /// Block `Header` values contain metadata about the block and about the
406    /// consensus, as well as commitments to the data in the current block, the
407    /// previous block, and the results returned by the application.
408    ///
409    /// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#header>
410    #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
411    pub struct BlockHeader {
412        /// Header version
413        pub version: HeaderVersion,
414
415        /// Chain ID
416        pub chain_id: String,
417
418        /// Current block height
419        pub height: u64,
420
421        /// Current timestamp
422        #[schemars(with = "String")]
423        #[schema(value_type = String)]
424        pub time: tendermint::Time,
425
426        /// Previous block info
427        pub last_block_id: Option<BlockId>,
428
429        /// Commit from validators from the last block
430        #[schemars(with = "Option<String>")]
431        #[schema(value_type = Option<String>)]
432        pub last_commit_hash: Option<Hash>,
433
434        /// Merkle root of transaction hashes
435        #[schemars(with = "Option<String>")]
436        #[schema(value_type = Option<String>)]
437        pub data_hash: Option<Hash>,
438
439        /// Validators for the current block
440        #[schemars(with = "String")]
441        #[schema(value_type = String)]
442        pub validators_hash: Hash,
443
444        /// Validators for the next block
445        #[schemars(with = "String")]
446        #[schema(value_type = String)]
447        pub next_validators_hash: Hash,
448
449        /// Consensus params for the current block
450        #[schemars(with = "String")]
451        #[schema(value_type = String)]
452        pub consensus_hash: Hash,
453
454        /// State after txs from the previous block
455        #[schemars(with = "String")]
456        #[schema(value_type = String)]
457        pub app_hash: Hash,
458
459        /// Root hash of all results from the txs from the previous block
460        #[schemars(with = "Option<String>")]
461        #[schema(value_type = Option<String>)]
462        pub last_results_hash: Option<Hash>,
463
464        /// Hash of evidence included in the block
465        #[schemars(with = "Option<String>")]
466        #[schema(value_type = Option<String>)]
467        pub evidence_hash: Option<Hash>,
468
469        /// Original proposer of the block
470        #[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
498//  implement required traits for the signer responses
499
500impl 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}